@ray-js/t-agent-plugin-aistream 0.2.8-beta.2 → 0.2.8-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.
- package/README-zh_CN.md +70 -11
- package/README.md +916 -706
- package/dist/AIStreamTypes.d.ts +20 -1
- package/dist/asr/AsrAgent.d.ts +1 -1
- package/dist/asr/AsrAgent.js +7 -5
- package/dist/buildIn/withBuildIn.d.ts +1 -0
- package/dist/buildIn/withBuildIn.js +149 -25
- package/dist/utils/AIStream.d.ts +15 -2
- package/dist/utils/AIStream.js +82 -40
- package/dist/utils/defaultMock.js +90 -3
- package/dist/utils/mock.d.ts +15 -0
- package/dist/utils/observer.d.ts +6 -0
- package/dist/utils/observer.js +100 -32
- package/dist/utils/sendMessage.d.ts +0 -1
- package/dist/utils/sendMessage.js +19 -8
- package/dist/utils/ttt.d.ts +1 -0
- package/dist/utils/ttt.js +2 -0
- package/dist/withAIStream.d.ts +10 -2
- package/dist/withAIStream.js +93 -57
- package/package.json +2 -3
package/dist/AIStreamTypes.d.ts
CHANGED
|
@@ -846,7 +846,7 @@ export type AudioBody = {
|
|
|
846
846
|
/** 接收数据通道 Code */
|
|
847
847
|
dataChannel: string;
|
|
848
848
|
/** 数据流类型: 0-仅一包, 1-传输开始, 2-传输中, 3-传输结束 */
|
|
849
|
-
streamFlag:
|
|
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
|
+
};
|
package/dist/asr/AsrAgent.d.ts
CHANGED
package/dist/asr/AsrAgent.js
CHANGED
|
@@ -96,9 +96,8 @@ export class AsrAgent {
|
|
|
96
96
|
return this.streamConn;
|
|
97
97
|
}
|
|
98
98
|
async createSession() {
|
|
99
|
-
|
|
100
|
-
|
|
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,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,135 @@ 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
|
+
// 注意:音频按流式分多包下发,播放所需的完整参数(path + 格式头
|
|
220
|
+
// codecType/sampleRate/channels/bitDepth)只在首包(START/ONLY_ONE)携带;
|
|
221
|
+
// 后续 END 等包里这些字段是 0(而非 null/undefined),会污染数据。
|
|
222
|
+
// 因此只取每段音频的首包,丢弃后续包,按音频流 id(part.id 在同一段音频的
|
|
223
|
+
// 所有分包间一致)去重。
|
|
224
|
+
onAttachmentsEnd((parts, respMsg) => {
|
|
225
|
+
const audioMap = new Map();
|
|
226
|
+
parts.forEach(i => {
|
|
227
|
+
if (i.attachmentType !== 'audio') {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const audio = i.attachment;
|
|
231
|
+
|
|
232
|
+
// 只接收首包,后续包直接丢弃
|
|
233
|
+
if (audio.streamFlag !== StreamFlag.START && audio.streamFlag !== StreamFlag.ONLY_ONE) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (audioMap.has(i.id)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
audioMap.set(i.id, {
|
|
240
|
+
path: audio.path,
|
|
241
|
+
pts: audio.pts,
|
|
242
|
+
codecType: audio.codecType,
|
|
243
|
+
sampleRate: audio.sampleRate,
|
|
244
|
+
channels: audio.channels,
|
|
245
|
+
bitDepth: audio.bitDepth
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
audioMap.forEach(audio => {
|
|
249
|
+
respMsg.bubble.addTile('bubbleTool', {
|
|
250
|
+
audio
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 播放结束 / 异常时,复位全局播放态
|
|
256
|
+
listenAudioPlayChanged(body => {
|
|
257
|
+
if (body.state === AudioPlayChangedState.COMPLETED || body.state === AudioPlayChangedState.ERROR) {
|
|
258
|
+
setPlayingMessage(null);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// 处理 UI 发来的播放/停止指令
|
|
263
|
+
onTileEvent(async (tile, payload) => {
|
|
264
|
+
var _tile$data;
|
|
265
|
+
if (tile.type !== 'bubbleTool' || (payload === null || payload === void 0 ? void 0 : payload.action) !== 'toggle') {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const audio = (_tile$data = tile.data) === null || _tile$data === void 0 ? void 0 : _tile$data.audio;
|
|
269
|
+
const messageId = tile.message.id;
|
|
270
|
+
const wasPlayingThis = session.get('AIStream.playingMessageId') === messageId;
|
|
271
|
+
|
|
272
|
+
// 先停掉当前正在播放的(无论是不是同一条)
|
|
273
|
+
await stopCurrent();
|
|
274
|
+
|
|
275
|
+
// 点击的是非播放态的按钮 -> 开始播放
|
|
276
|
+
if (!wasPlayingThis && audio) {
|
|
277
|
+
await playAudio(audio);
|
|
278
|
+
await setPlayingMessage(messageId);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
})();
|
|
158
282
|
return {};
|
|
159
283
|
};
|
|
160
284
|
}
|
package/dist/utils/AIStream.d.ts
CHANGED
|
@@ -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
|
-
|
|
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?;
|
package/dist/utils/AIStream.js
CHANGED
|
@@ -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, "
|
|
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.
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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.
|
|
135
|
-
|
|
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.
|
|
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.
|
|
190
|
+
this.unregisterSession(session);
|
|
191
|
+
this.allSessions.delete(session);
|
|
192
|
+
await this.client.disposeIfIdle(this);
|
|
150
193
|
}
|
|
151
|
-
|
|
152
|
-
// 断连
|
|
153
194
|
async close() {
|
|
154
|
-
|
|
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 =
|
|
287
|
+
const options = {
|
|
253
288
|
bizTag: BizTag.DEFAULT,
|
|
254
|
-
ownerId
|
|
255
|
-
|
|
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$
|
|
573
|
-
if (((_context$
|
|
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
|
}
|
package/dist/utils/mock.d.ts
CHANGED
|
@@ -23,6 +23,21 @@ export interface SendToAIStreamContext {
|
|
|
23
23
|
writeSkill: (skill: ReceivedTextSkillPacketBody, options?: {
|
|
24
24
|
delayMs?: number;
|
|
25
25
|
}) => Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* 模拟一段流式音频(TTS)下发。
|
|
28
|
+
* 真实协议里格式头(codecType/sampleRate/channels/bitDepth)只在 START 包携带,
|
|
29
|
+
* END 包通常只带最终 path/pts,这里据实模拟,分两包下发。
|
|
30
|
+
*/
|
|
31
|
+
writeAudio: (audio: {
|
|
32
|
+
path: string;
|
|
33
|
+
codecType?: number;
|
|
34
|
+
sampleRate?: number;
|
|
35
|
+
channels?: number;
|
|
36
|
+
bitDepth?: number;
|
|
37
|
+
pts?: number;
|
|
38
|
+
}, options?: {
|
|
39
|
+
delayMs?: number;
|
|
40
|
+
}) => Promise<void>;
|
|
26
41
|
end: (options?: {
|
|
27
42
|
delayMs?: number;
|
|
28
43
|
}) => Promise<void>;
|