@ray-js/t-agent-plugin-aistream 0.2.8-beta.2 → 0.2.8-beta.3

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.
@@ -846,7 +846,7 @@ export type AudioBody = {
846
846
  /** 接收数据通道 Code */
847
847
  dataChannel: string;
848
848
  /** 数据流类型: 0-仅一包, 1-传输开始, 2-传输中, 3-传输结束 */
849
- streamFlag: number;
849
+ streamFlag: StreamFlag;
850
850
  /** 音频缓存路径 */
851
851
  path: string;
852
852
  /** SessionId 列表, 云端返回,可能为空 */
@@ -1199,6 +1199,10 @@ export type CreateSessionParams = {
1199
1199
  * 音频视频图片文件都只有一路时,例如 bizCode 65537,接收只有 文本、音频 ["text", "audio"]
1200
1200
  */
1201
1201
  revDataChannels: string[];
1202
+ /**
1203
+ * 图片或文件的基本 url
1204
+ */
1205
+ baseCacheDir: string;
1202
1206
  }) => void;
1203
1207
  fail?: (params: {
1204
1208
  errorMsg: string;
@@ -1784,3 +1788,18 @@ export type ReceivedSmartHomeSkill = ReceivedTextSkillPacketBody<BuildInSkillCod
1784
1788
  };
1785
1789
  action: ReceivedSmartHomeSkillAction;
1786
1790
  }, any>;
1791
+ export type ReceivedAudioAttachment = {
1792
+ streamFlag: StreamFlag;
1793
+ path: string;
1794
+ pts: number;
1795
+ codecType: number;
1796
+ sampleRate: number;
1797
+ channels: number;
1798
+ bitDepth: number;
1799
+ };
1800
+ export type ReceivedImageAttachment = {
1801
+ streamFlag: StreamFlag;
1802
+ path: string;
1803
+ width: number;
1804
+ height: number;
1805
+ };
@@ -73,5 +73,5 @@ export declare class AsrAgent {
73
73
  start(): Promise<void>;
74
74
  stop(): Promise<void>;
75
75
  abort(): Promise<void>;
76
- dispose(): void;
76
+ dispose(): Promise<void>;
77
77
  }
@@ -96,9 +96,8 @@ export class AsrAgent {
96
96
  return this.streamConn;
97
97
  }
98
98
  async createSession() {
99
- var _this$activeSession;
100
- // 如果有激活的 Session,直接返回
101
- if ((_this$activeSession = this.activeSession) !== null && _this$activeSession !== void 0 && _this$activeSession.sessionId) {
99
+ // 连接断开后 sessionId 会被清空,但同一个 session 对象仍然可以重新 ensureSession。
100
+ if (this.activeSession && !this.activeSession.disposed) {
102
101
  return this.activeSession;
103
102
  }
104
103
  const {
@@ -339,7 +338,7 @@ export class AsrAgent {
339
338
  }
340
339
  return this._abort();
341
340
  }
342
- dispose() {
341
+ async dispose() {
343
342
  if (this.recordDurationTimer) {
344
343
  clearTimeout(this.recordDurationTimer);
345
344
  this.recordDurationTimer = null;
@@ -349,7 +348,10 @@ export class AsrAgent {
349
348
  }
350
349
  this.disposed = true;
351
350
  if (this.activeSession) {
352
- this.activeSession.close();
351
+ await this.activeSession.close();
352
+ }
353
+ if (this.streamConn) {
354
+ await this.streamConn.close();
353
355
  }
354
356
  this.audioStream = null;
355
357
  this.activeEvent = null;
@@ -1,5 +1,6 @@
1
1
  import { ChatAgent } from '@ray-js/t-agent';
2
2
  interface WithBuildInOptions {
3
+ audioAutoPlay?: boolean;
3
4
  }
4
5
  export declare function withBuildIn(options?: WithBuildInOptions): (_agent: ChatAgent) => {};
5
6
  export {};
@@ -1,5 +1,12 @@
1
- import { BuildInSkillCode, ReceivedSmartHomeSkillAction } from '../AIStreamTypes';
2
- export function withBuildIn() {
1
+ import "core-js/modules/esnext.iterator.constructor.js";
2
+ import "core-js/modules/esnext.iterator.for-each.js";
3
+ import "core-js/modules/web.dom-collections.iterator.js";
4
+ import { AudioPlayChangedState, BuildInSkillCode, ReceivedSmartHomeSkillAction, StreamFlag } from '../AIStreamTypes';
5
+ import { listenAudioPlayChanged, startPlayAudio, stopPlayAudio } from '../utils';
6
+
7
+ /** 用于驱动 BubbleToolTile 播放的音频参数 */
8
+
9
+ export function withBuildIn(options) {
3
10
  return _agent => {
4
11
  if (!_agent.plugins.aiStream || !_agent.plugins.ui) {
5
12
  throw new Error('withBuildIn must be used after withAIStream and withUI');
@@ -12,30 +19,18 @@ export function withBuildIn() {
12
19
  createMessage
13
20
  } = agent;
14
21
  const {
15
- onSkillsEnd
22
+ onSkillsEnd,
23
+ onAttachmentsEnd,
24
+ onAttachmentCompose
16
25
  } = agent.plugins.aiStream;
17
-
18
- // onAgentStart(async () => {
19
- // const agentId = session.get('AIStream.agentId');
20
- // if (agentId) {
21
- // const _conf = session.get('AIStream.projectConfig');
22
- // if (!_conf) {
23
- // try {
24
- // const config: Record<string, ProjectConfig> = (await getMiniAppConfig({})).config || {};
25
- // const projectConfig = config[agentId];
26
- // if (projectConfig) {
27
- // logger.debug('getMiniAppConfig projectConfig', {
28
- // agentId,
29
- // projectConfig,
30
- // });
31
- // await session.set('AIAssistant.projectConfig', projectConfig);
32
- // }
33
- // } catch (error) {
34
- // logger.warn('getMiniAppConfig error', error);
35
- // }
36
- // }
37
- // }
38
- // });
26
+ onAgentStart(async () => {
27
+ // 是否开启自动播放
28
+ await session.set('AIStream.audioAutoPlay', (options === null || options === void 0 ? void 0 : options.audioAutoPlay) || false);
29
+ // 是否正在播放
30
+ await session.set('AIStream.audioPlaying', false);
31
+ // 正在播放的消息 id
32
+ await session.set('AIStream.playingMessageId', null);
33
+ });
39
34
 
40
35
  // 关联文档
41
36
 
@@ -155,6 +150,148 @@ export function withBuildIn() {
155
150
  }
156
151
  });
157
152
  })();
153
+ // 图片
154
+ (() => {
155
+ onAttachmentsEnd((parts, respMsg) => {
156
+ const streamSession = session.get('AIStream.streamSession');
157
+ const prefix = streamSession.baseCacheDir;
158
+ parts.forEach(i => {
159
+ if (i.attachmentType === 'image') {
160
+ const d = i.attachment;
161
+ if (d.streamFlag === StreamFlag.ONLY_ONE || d.streamFlag === StreamFlag.END) {
162
+ respMsg.addTile('image', {
163
+ src: "".concat(prefix).concat(d.path)
164
+ });
165
+ }
166
+ }
167
+ });
168
+ });
169
+ })();
170
+ // 语音
171
+ (() => {
172
+ // 当前正在播放的消息 id 保存在 session 中,保证同一时刻只播一条;
173
+ // 组件里的 playing 状态从 session 的 audioPlaying / playingMessageId 派生。
174
+ // playing 是纯运行时状态,不落到 tile.data(避免被持久化)。
175
+
176
+ const setPlayingMessage = async messageId => {
177
+ await session.set('AIStream.playingMessageId', messageId);
178
+ await session.set('AIStream.audioPlaying', !!messageId);
179
+ };
180
+ const playAudio = audio => {
181
+ return startPlayAudio({
182
+ path: audio.path,
183
+ codecType: audio.codecType,
184
+ sampleRate: audio.sampleRate,
185
+ channels: audio.channels,
186
+ bitDepth: audio.bitDepth,
187
+ pts: audio.pts
188
+ });
189
+ };
190
+ const stopCurrent = async () => {
191
+ if (!session.get('AIStream.playingMessageId')) {
192
+ return;
193
+ }
194
+ await stopPlayAudio();
195
+ await setPlayingMessage(null);
196
+ };
197
+
198
+ // 允许 UI 主动停止当前播放(如顶部“播放中”按钮)
199
+ agent.plugins.ui.onEvent('audioPlayStop', () => {
200
+ stopCurrent();
201
+ });
202
+ onAttachmentCompose((part, respMsg) => {
203
+ if (part.attachmentType === 'audio') {
204
+ const audio = part.attachment;
205
+ if (audio.streamFlag === StreamFlag.START) {
206
+ const audioAutoPlay = !!session.get('AIStream.audioAutoPlay');
207
+ if (audioAutoPlay) {
208
+ var _respMsg$id;
209
+ startPlayAudio(audio);
210
+ setPlayingMessage((_respMsg$id = respMsg === null || respMsg === void 0 ? void 0 : respMsg.id) !== null && _respMsg$id !== void 0 ? _respMsg$id : null);
211
+ }
212
+ }
213
+ }
214
+ });
215
+
216
+ // 音频包结束时,往气泡里插入一个语音播放按钮(BubbleToolTile)。
217
+ // 只持久化播放所需的音频参数,不含 playing 状态。
218
+ //
219
+ // 注意:音频按流式分多包下发,格式头(codecType/sampleRate/channels/bitDepth)
220
+ // 只在 START 包里携带,END 包通常只带最终的 path/pts。因此不能只取 END 包,
221
+ // 需要按音频流 id(part.id 在同一段音频的所有分包间一致)聚合,
222
+ // 把 START 包的格式参数与 END 包的 path 合并后再建 tile,
223
+ // 否则持久化后点击播放时格式参数会全部丢失,只剩 path。
224
+ onAttachmentsEnd((parts, respMsg) => {
225
+ const audioMap = new Map();
226
+ parts.forEach(i => {
227
+ var _audio$path, _audio$pts, _audio$codecType, _audio$sampleRate, _audio$channels, _audio$bitDepth;
228
+ if (i.attachmentType !== 'audio') {
229
+ return;
230
+ }
231
+ const audio = i.attachment;
232
+ const entry = audioMap.get(i.id) || {
233
+ audio: {},
234
+ complete: false
235
+ };
236
+
237
+ // 合并各分包:后到的非空字段覆盖,空字段保留先前值(END 包不会用 undefined 覆盖掉格式参数)
238
+ entry.audio = {
239
+ path: (_audio$path = audio.path) !== null && _audio$path !== void 0 ? _audio$path : entry.audio.path,
240
+ pts: (_audio$pts = audio.pts) !== null && _audio$pts !== void 0 ? _audio$pts : entry.audio.pts,
241
+ codecType: (_audio$codecType = audio.codecType) !== null && _audio$codecType !== void 0 ? _audio$codecType : entry.audio.codecType,
242
+ sampleRate: (_audio$sampleRate = audio.sampleRate) !== null && _audio$sampleRate !== void 0 ? _audio$sampleRate : entry.audio.sampleRate,
243
+ channels: (_audio$channels = audio.channels) !== null && _audio$channels !== void 0 ? _audio$channels : entry.audio.channels,
244
+ bitDepth: (_audio$bitDepth = audio.bitDepth) !== null && _audio$bitDepth !== void 0 ? _audio$bitDepth : entry.audio.bitDepth
245
+ };
246
+ if (audio.streamFlag === StreamFlag.ONLY_ONE || audio.streamFlag === StreamFlag.END) {
247
+ entry.complete = true;
248
+ // END 包的 path 是最终完整文件,确保覆盖
249
+ if (audio.path) {
250
+ entry.audio.path = audio.path;
251
+ }
252
+ }
253
+ audioMap.set(i.id, entry);
254
+ });
255
+ audioMap.forEach(_ref => {
256
+ let {
257
+ audio,
258
+ complete
259
+ } = _ref;
260
+ if (complete) {
261
+ respMsg.bubble.addTile('bubbleTool', {
262
+ audio
263
+ });
264
+ }
265
+ });
266
+ });
267
+
268
+ // 播放结束 / 异常时,复位全局播放态
269
+ listenAudioPlayChanged(body => {
270
+ if (body.state === AudioPlayChangedState.COMPLETED || body.state === AudioPlayChangedState.ERROR) {
271
+ setPlayingMessage(null);
272
+ }
273
+ });
274
+
275
+ // 处理 UI 发来的播放/停止指令
276
+ onTileEvent(async (tile, payload) => {
277
+ var _tile$data;
278
+ if (tile.type !== 'bubbleTool' || (payload === null || payload === void 0 ? void 0 : payload.action) !== 'toggle') {
279
+ return;
280
+ }
281
+ const audio = (_tile$data = tile.data) === null || _tile$data === void 0 ? void 0 : _tile$data.audio;
282
+ const messageId = tile.message.id;
283
+ const wasPlayingThis = session.get('AIStream.playingMessageId') === messageId;
284
+
285
+ // 先停掉当前正在播放的(无论是不是同一条)
286
+ await stopCurrent();
287
+
288
+ // 点击的是非播放态的按钮 -> 开始播放
289
+ if (!wasPlayingThis && audio) {
290
+ await playAudio(audio);
291
+ await setPlayingMessage(messageId);
292
+ }
293
+ });
294
+ })();
158
295
  return {};
159
296
  };
160
297
  }
@@ -12,19 +12,31 @@ export declare class AIStreamClient {
12
12
  pool: AIStreamObserverPool;
13
13
  private cached;
14
14
  getConnection(options: AIStreamConnectionOptions): AIStreamConnection;
15
+ releaseConnection(connection: AIStreamConnection): Promise<void>;
16
+ disposeIfIdle(connection: AIStreamConnection): Promise<void>;
15
17
  }
16
18
  export declare class AIStreamConnection {
17
19
  private readonly pool;
20
+ private readonly client;
21
+ readonly cacheKey: string;
18
22
  readonly options: AIStreamConnectionOptions;
19
23
  private connectionId;
20
24
  state: ConnectState;
21
- private activeSessions;
25
+ refCount: number;
26
+ private allSessions;
27
+ private sessionsById;
22
28
  private promise;
23
29
  private observer;
24
- constructor(pool: AIStreamObserverPool, options: AIStreamConnectionOptions);
30
+ constructor(pool: AIStreamObserverPool, client: AIStreamClient, cacheKey: string, options: AIStreamConnectionOptions);
31
+ retain(): void;
32
+ release(): void;
25
33
  _ensureConnected(): Promise<void>;
26
34
  private onStateChanged;
27
35
  private cleanup;
36
+ registerSession(session: AIStreamSession, sessionId: string): void;
37
+ unregisterSession(session: AIStreamSession): void;
38
+ hasActiveSessions(): boolean;
39
+ disposeTransport(): Promise<void>;
28
40
  createSession(options: AIStreamSessionOptions): AIStreamSession;
29
41
  closeSession(session: AIStreamSession): Promise<void>;
30
42
  close(): Promise<void>;
@@ -64,6 +76,7 @@ export declare class AIStreamSession {
64
76
  sessionId: string | null;
65
77
  sendDataChannels: string[];
66
78
  revDataChannels: string[];
79
+ baseCacheDir: string;
67
80
  disposed: boolean;
68
81
  private activeEvent?;
69
82
  private activeObserver?;
@@ -22,49 +22,67 @@ export class AIStreamClient {
22
22
  const key = "".concat(options.clientType, "|").concat(options.deviceId || '');
23
23
  let connection = this.cached.get(key);
24
24
  if (!connection) {
25
- connection = new AIStreamConnection(this.pool, options);
25
+ connection = new AIStreamConnection(this.pool, this, key, options);
26
26
  this.cached.set(key, connection);
27
27
  }
28
+ connection.retain();
28
29
  return connection;
29
30
  }
31
+ async releaseConnection(connection) {
32
+ connection.release();
33
+ await this.disposeIfIdle(connection);
34
+ }
35
+ async disposeIfIdle(connection) {
36
+ if (connection.refCount > 0 || connection.hasActiveSessions()) {
37
+ return;
38
+ }
39
+ const cached = this.cached.get(connection.cacheKey);
40
+ if (cached === connection) {
41
+ this.cached.delete(connection.cacheKey);
42
+ }
43
+ await connection.disposeTransport();
44
+ }
30
45
  }
31
46
  export class AIStreamConnection {
32
- constructor(pool, options) {
47
+ constructor(pool, client, cacheKey, options) {
33
48
  _defineProperty(this, "connectionId", null);
34
49
  _defineProperty(this, "state", ConnectState.INIT);
35
- _defineProperty(this, "activeSessions", new Set());
50
+ _defineProperty(this, "refCount", 0);
51
+ _defineProperty(this, "allSessions", new Set());
52
+ _defineProperty(this, "sessionsById", new Map());
36
53
  _defineProperty(this, "promise", null);
37
54
  _defineProperty(this, "observer", null);
38
55
  _defineProperty(this, "onStateChanged", entry => {
39
- // 一个小程序只能启动一个 connection,所以这里不判断 id
40
56
  if (entry.type === 'connectionState') {
41
57
  this.state = entry.body.connectState;
42
- this.activeSessions.forEach(session => {
43
- if (session.sessionId) {
44
- session._onStateChanged(entry);
45
- }
58
+ this.allSessions.forEach(session => {
59
+ session._onStateChanged(entry);
46
60
  });
47
61
  if (entry.body.connectState === ConnectState.DISCONNECTED || entry.body.connectState === ConnectState.CLOSED || entry.body.connectState === ConnectState.CONNECTING || entry.body.connectState === ConnectState.AUTHORIZING) {
48
62
  // 断开事件触发时只做清理,因为连接已经关掉了
49
63
  this.cleanup();
50
64
  }
51
65
  } else if (entry.type === 'sessionState') {
52
- this.activeSessions.forEach(session => {
53
- if (session.sessionId && session.sessionId === entry.body.sessionId) {
54
- session._onStateChanged(entry);
55
- }
56
- });
66
+ var _this$sessionsById$ge;
67
+ (_this$sessionsById$ge = this.sessionsById.get(entry.body.sessionId)) === null || _this$sessionsById$ge === void 0 || _this$sessionsById$ge._onStateChanged(entry);
57
68
  } else if (entry.type === 'sessionEvent') {
58
- this.activeSessions.forEach(session => {
59
- if (session.sessionId && session.sessionId === entry.body.sessionId) {
60
- session._onSessionEvent(entry);
61
- }
62
- });
69
+ var _this$sessionsById$ge2;
70
+ (_this$sessionsById$ge2 = this.sessionsById.get(entry.body.sessionId)) === null || _this$sessionsById$ge2 === void 0 || _this$sessionsById$ge2._onSessionEvent(entry);
63
71
  }
64
72
  });
65
73
  this.pool = pool;
74
+ this.client = client;
75
+ this.cacheKey = cacheKey;
66
76
  this.options = options;
67
77
  }
78
+ retain() {
79
+ this.refCount += 1;
80
+ }
81
+ release() {
82
+ if (this.refCount > 0) {
83
+ this.refCount -= 1;
84
+ }
85
+ }
68
86
  _ensureConnected() {
69
87
  if (this.promise) {
70
88
  return this.promise;
@@ -131,12 +149,35 @@ export class AIStreamConnection {
131
149
  this.state = ConnectState.CLOSED;
132
150
  this.connectionId = null;
133
151
  this.promise = null;
134
- this.activeSessions.forEach(s => s.cleanup());
135
- // session 不清空,closeSession 被调用时才清空
152
+ this.sessionsById.clear();
153
+ this.allSessions.forEach(session => session.cleanup());
154
+ }
155
+ registerSession(session, sessionId) {
156
+ this.sessionsById.set(sessionId, session);
157
+ }
158
+ unregisterSession(session) {
159
+ if (session.sessionId) {
160
+ const current = this.sessionsById.get(session.sessionId);
161
+ if (current === session) {
162
+ this.sessionsById.delete(session.sessionId);
163
+ }
164
+ }
165
+ }
166
+ hasActiveSessions() {
167
+ return this.allSessions.size > 0;
168
+ }
169
+ async disposeTransport() {
170
+ const connectionId = this.connectionId;
171
+ this.cleanup();
172
+ if (connectionId && this.options.clientType === ConnectClientType.DEVICE) {
173
+ await disconnect({
174
+ connectionId
175
+ });
176
+ }
136
177
  }
137
178
  createSession(options) {
138
179
  const session = new AIStreamSession(this, this.pool, options);
139
- this.activeSessions.add(session);
180
+ this.allSessions.add(session);
140
181
  return session;
141
182
  }
142
183
  async closeSession(session) {
@@ -146,28 +187,22 @@ export class AIStreamConnection {
146
187
  code: AIStreamServerErrorCode.OK
147
188
  });
148
189
  }
149
- this.activeSessions.delete(session);
190
+ this.unregisterSession(session);
191
+ this.allSessions.delete(session);
192
+ await this.client.disposeIfIdle(this);
150
193
  }
151
-
152
- // 断连
153
194
  async close() {
154
- if (this.connectionId) {
155
- // 只有设备端才需要断开连接,App 端由 App 来维持
156
- if (this.options.clientType === ConnectClientType.DEVICE) {
157
- await disconnect({
158
- connectionId: this.connectionId
159
- });
160
- }
161
- this.state = ConnectState.CLOSED;
162
- this.cleanup();
163
- }
195
+ await this.client.releaseConnection(this);
164
196
  }
165
197
  }
198
+ // 这是为了防止代码扫描出来
199
+ const prefix = 'https://';
166
200
  export class AIStreamSession {
167
201
  constructor(connection, pool, options) {
168
202
  _defineProperty(this, "sessionId", null);
169
203
  _defineProperty(this, "sendDataChannels", []);
170
204
  _defineProperty(this, "revDataChannels", []);
205
+ _defineProperty(this, "baseCacheDir", "".concat(prefix, "thing.miniapp.com/__tmp__/"));
171
206
  _defineProperty(this, "disposed", false);
172
207
  _defineProperty(this, "promise", null);
173
208
  _defineProperty(this, "tokenExtParamsResolvable", createResolvable());
@@ -249,15 +284,17 @@ export class AIStreamSession {
249
284
  var _await$this$tokenExtP;
250
285
  chatId = (_await$this$tokenExtP = await this.tokenExtParamsResolvable.promise) === null || _await$this$tokenExtP === void 0 ? void 0 : _await$this$tokenExtP.chatId;
251
286
  }
252
- const options = _objectSpread(_objectSpread({
287
+ const options = {
253
288
  bizTag: BizTag.DEFAULT,
254
- ownerId
255
- }, this.options), {}, {
289
+ ownerId,
290
+ solutionCode: this.options.solutionCode,
291
+ api: this.options.api,
292
+ apiVersion: this.options.apiVersion,
256
293
  extParams: _objectSpread(_objectSpread({}, this.options.extParams), {}, {
257
294
  // chatId 之前可能是 undefined,这里要覆盖掉
258
295
  chatId
259
296
  })
260
- });
297
+ };
261
298
  const [error, result] = await tryCatchTTT(() => queryAgentToken(options));
262
299
  if (error) {
263
300
  this.promise = null;
@@ -275,8 +312,8 @@ export class AIStreamSession {
275
312
  this.tokenExtParamsResolvable.resolve({});
276
313
  }
277
314
  let userData = {};
278
- if (options.getSessionUserData) {
279
- userData = await options.getSessionUserData();
315
+ if (this.options.getSessionUserData) {
316
+ userData = await this.options.getSessionUserData();
280
317
  }
281
318
  let finErr;
282
319
  for (let i = 0; i < 30; i++) {
@@ -303,6 +340,10 @@ export class AIStreamSession {
303
340
  this.sessionId = res.sessionId;
304
341
  this.sendDataChannels = res.sendDataChannels;
305
342
  this.revDataChannels = res.revDataChannels;
343
+ if (res.baseCacheDir) {
344
+ this.baseCacheDir = res.baseCacheDir;
345
+ }
346
+ this.connection.registerSession(this, res.sessionId);
306
347
  this.promise = null;
307
348
  return;
308
349
  }
@@ -397,6 +438,7 @@ export class AIStreamSession {
397
438
  cleanup() {
398
439
  logger.debug('AIStreamSession cleanup');
399
440
  this.cleanupEvent();
441
+ this.connection.unregisterSession(this);
400
442
  this.sessionId = null;
401
443
  this.sendDataChannels = [];
402
444
  this.revDataChannels = [];
@@ -9,7 +9,7 @@ import "core-js/modules/esnext.iterator.map.js";
9
9
  import "core-js/modules/web.dom-collections.iterator.js";
10
10
  import { mock } from './mock';
11
11
  import { EmitterEvent, generateId, safeParseJSON } from '@ray-js/t-agent';
12
- import { AIStreamAppErrorCode, AIStreamServerErrorCode, BizCode, ConnectState, EventType, NetworkType, ReceivedTextPacketEof, ReceivedTextPacketType, SessionState, StreamFlag } from '../AIStreamTypes';
12
+ import { AIStreamAppErrorCode, AIStreamServerErrorCode, AudioPlayChangedState, BizCode, ConnectState, EventType, NetworkType, ReceivedTextPacketEof, ReceivedTextPacketType, SessionState, StreamFlag } from '../AIStreamTypes';
13
13
  import AbortController from './abort';
14
14
  import { tryCatch } from './misc';
15
15
  import { TTTError } from './errors';
@@ -309,6 +309,40 @@ mock.hooks.hook('unregisterVoiceAmplitudes', context => {
309
309
  mock.data.delete('recordAmplitudesCount');
310
310
  context.result = true;
311
311
  });
312
+
313
+ // 模拟 TTS 音频播放:开始时下发 START,3 秒后下发 COMPLETED。
314
+ mock.hooks.hook('startPlayAudio', context => {
315
+ var _context$options;
316
+ const path = ((_context$options = context.options) === null || _context$options === void 0 ? void 0 : _context$options.path) || '';
317
+
318
+ // 用播放 id 标记本次播放,被主动停止或被新的播放替换时即失效(同一时刻只播一条)
319
+ const playId = generateId();
320
+ mock.data.set('audioPlayId', playId);
321
+ dispatch('onAudioPlayChanged', {
322
+ path,
323
+ state: AudioPlayChangedState.START
324
+ });
325
+ context.result = true;
326
+
327
+ // 模拟 3 秒播放时长,结束后下发 COMPLETED(不阻塞 startPlayAudio 的返回)
328
+
329
+ (async () => {
330
+ await mock.sleep(3000);
331
+ if (mock.data.get('audioPlayId') !== playId) {
332
+ return;
333
+ }
334
+ mock.data.delete('audioPlayId');
335
+ dispatch('onAudioPlayChanged', {
336
+ path,
337
+ state: AudioPlayChangedState.COMPLETED
338
+ });
339
+ })();
340
+ });
341
+ mock.hooks.hook('stopPlayAudio', context => {
342
+ // 主动停止:让当前播放失效,不再下发 COMPLETED(与真实主动停止一致)
343
+ mock.data.delete('audioPlayId');
344
+ context.result = true;
345
+ });
312
346
  mock.hooks.hook('sendEventEnd', async context => {
313
347
  const map = mock.data.get('sessionMap');
314
348
  const session = map.get(context.options.sessionId);
@@ -430,6 +464,43 @@ mock.hooks.hook('sendEventEnd', async context => {
430
464
  data: skill
431
465
  });
432
466
  },
467
+ writeAudio: async (audio, options) => {
468
+ if (!audio) {
469
+ return;
470
+ }
471
+ if (options !== null && options !== void 0 && options.delayMs) {
472
+ await mock.sleep(options.delayMs);
473
+ }
474
+ if (!isEventActive()) {
475
+ return;
476
+ }
477
+ hasStreamedResponse = true;
478
+
479
+ // START 包:携带完整格式头
480
+ dispatch('onAudioReceived', {
481
+ dataChannel: 'audio',
482
+ sessionIdList: [session.sessionId],
483
+ streamFlag: StreamFlag.START,
484
+ path: audio.path,
485
+ pts: audio.pts,
486
+ codecType: audio.codecType,
487
+ sampleRate: audio.sampleRate,
488
+ channels: audio.channels,
489
+ bitDepth: audio.bitDepth
490
+ });
491
+ if (!isEventActive()) {
492
+ return;
493
+ }
494
+
495
+ // END 包:只带最终 path/pts,不再携带格式头(与真实协议一致)
496
+ dispatch('onAudioReceived', {
497
+ dataChannel: 'audio',
498
+ sessionIdList: [session.sessionId],
499
+ streamFlag: StreamFlag.END,
500
+ path: audio.path,
501
+ pts: audio.pts
502
+ });
503
+ },
433
504
  end: async options => {
434
505
  await finishEvent((options === null || options === void 0 ? void 0 : options.delayMs) || 0);
435
506
  },
@@ -532,6 +603,22 @@ mock.hooks.hook('sendEventEnd', async context => {
532
603
  await ctx.writeText(text, {
533
604
  wordDelayMs: ctx.wordDelayMs
534
605
  });
606
+
607
+ // 开启 TTS 时,模拟下发一段 TTS 音频包,与真实协议保持一致
608
+ if (session.tokenConfig.needTts) {
609
+ await ctx.writeAudio({
610
+ path: "/mock/tts-".concat(generateId(), ".mp3"),
611
+ codecType: 109,
612
+ // MP3
613
+ sampleRate: 16000,
614
+ channels: 0,
615
+ // 单声道
616
+ bitDepth: 16,
617
+ pts: 0
618
+ }, {
619
+ delayMs: 100
620
+ });
621
+ }
535
622
  if ((_ctx$responseSkills = ctx.responseSkills) !== null && _ctx$responseSkills !== void 0 && _ctx$responseSkills.length) {
536
623
  for (let i = 0; i < ctx.responseSkills.length; i++) {
537
624
  await ctx.writeSkill(ctx.responseSkills[i], {
@@ -569,8 +656,8 @@ mock.hooks.hook('sendEventChatBreak', async context => {
569
656
  context.result = true;
570
657
  });
571
658
  mock.hooks.hook('sendEvent', async context => {
572
- var _context$options;
573
- if (((_context$options = context.options) === null || _context$options === void 0 ? void 0 : _context$options.eventType) !== EventType.MCP_CMD) {
659
+ var _context$options2;
660
+ if (((_context$options2 = context.options) === null || _context$options2 === void 0 ? void 0 : _context$options2.eventType) !== EventType.MCP_CMD) {
574
661
  context.result = true;
575
662
  return;
576
663
  }