sa2kit 1.6.49 → 1.6.52

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,281 @@
1
+ 'use strict';
2
+
3
+ require('../chunk-Z6ZWNWWR.js');
4
+ var crypto = require('crypto');
5
+ var WebSocket = require('ws');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
10
+ var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
11
+
12
+ var IflytekServerAdapter = class {
13
+ constructor(config) {
14
+ this.sessions = /* @__PURE__ */ new Map();
15
+ this.appId = config.appId;
16
+ this.apiKey = config.apiKey;
17
+ this.apiSecret = config.apiSecret;
18
+ this.host = config.host ?? "iat-api.xfyun.cn";
19
+ this.path = config.path ?? "/v2/iat";
20
+ this.debug = config.debug ?? false;
21
+ }
22
+ /**
23
+ * 将适配器绑定到一个 Socket.IO socket 连接上。
24
+ * 自动监听 iflytek:start / iflytek:audio / iflytek:stop / disconnect 事件。
25
+ */
26
+ attach(socket) {
27
+ socket.on("iflytek:start", (payload) => {
28
+ this.handleStart(socket, payload);
29
+ });
30
+ socket.on("iflytek:audio", (frame) => {
31
+ this.handleAudio(socket, frame);
32
+ });
33
+ socket.on("iflytek:stop", (payload) => {
34
+ this.handleStop(socket, payload);
35
+ });
36
+ socket.on("disconnect", () => {
37
+ this.handleDisconnect(socket);
38
+ });
39
+ }
40
+ // ─── 事件处理 ───
41
+ handleStart(socket, payload) {
42
+ const sessionId = payload?.sessionId ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
43
+ if (!this.appId || !this.apiKey || !this.apiSecret) {
44
+ socket.emit("iflytek:error", {
45
+ sessionId,
46
+ message: "\u670D\u52A1\u7AEF\u672A\u914D\u7F6E IFLYTEK_APP_ID / IFLYTEK_API_KEY / IFLYTEK_API_SECRET"
47
+ });
48
+ return;
49
+ }
50
+ const existing = this.sessions.get(socket.id);
51
+ if (existing) {
52
+ if (existing.ended) {
53
+ this.log(socket.id, "replace ended session");
54
+ this.closeUpstream(existing);
55
+ this.sessions.delete(socket.id);
56
+ } else if (existing.ws.readyState === WebSocket__default.default.OPEN) {
57
+ this.log(socket.id, "duplicate start ignored (session open)");
58
+ socket.emit("iflytek:ready", { sessionId: existing.sessionId });
59
+ return;
60
+ } else if (existing.ws.readyState === WebSocket__default.default.CONNECTING) {
61
+ this.log(socket.id, "duplicate start ignored (session connecting)");
62
+ return;
63
+ } else {
64
+ this.sessions.delete(socket.id);
65
+ }
66
+ }
67
+ const wsUrl = this.buildWsUrl();
68
+ const ws = new WebSocket__default.default(wsUrl);
69
+ const state = {
70
+ ws,
71
+ sessionId,
72
+ frameCount: 0,
73
+ firstFrameSent: false,
74
+ ended: false,
75
+ resultText: "",
76
+ resultSegments: []
77
+ };
78
+ this.sessions.set(socket.id, state);
79
+ this.log(socket.id, "start session", { sessionId });
80
+ ws.on("open", () => {
81
+ this.log(socket.id, "upstream open");
82
+ socket.emit("iflytek:ready", { sessionId });
83
+ });
84
+ ws.on("message", (raw) => {
85
+ try {
86
+ const msg = JSON.parse(String(raw));
87
+ this.log(socket.id, "upstream message", {
88
+ code: msg.code,
89
+ status: msg?.data?.status
90
+ });
91
+ if (msg.code !== 0) {
92
+ socket.emit("iflytek:error", {
93
+ sessionId,
94
+ message: msg.message || `\u8BAF\u98DE\u8FD4\u56DE\u9519\u8BEF code=${msg.code}`
95
+ });
96
+ return;
97
+ }
98
+ const current = this.sessions.get(socket.id);
99
+ if (!current || current.ws !== ws) return;
100
+ const result = this.extractResult(msg.data?.result);
101
+ const mergedText = this.mergeResult(current, result);
102
+ const isFinal = msg.data?.status === 2;
103
+ if (mergedText || isFinal) {
104
+ socket.emit("iflytek:result", {
105
+ sessionId,
106
+ text: mergedText,
107
+ isFinal
108
+ });
109
+ }
110
+ if (isFinal) {
111
+ const current2 = this.sessions.get(socket.id);
112
+ if (current2?.ws === ws) {
113
+ this.closeUpstream(current2);
114
+ }
115
+ }
116
+ } catch (e) {
117
+ socket.emit("iflytek:error", { sessionId, message: e.message });
118
+ }
119
+ });
120
+ ws.on("error", (e) => {
121
+ if (e?.message?.includes("before the connection was established")) {
122
+ this.log(socket.id, "ignore early-close error");
123
+ return;
124
+ }
125
+ socket.emit("iflytek:error", {
126
+ sessionId,
127
+ message: e?.message || "\u8BAF\u98DE WebSocket \u8FDE\u63A5\u5931\u8D25"
128
+ });
129
+ });
130
+ ws.on("close", () => {
131
+ this.log(socket.id, "upstream close");
132
+ const current = this.sessions.get(socket.id);
133
+ if (current?.ws === ws) {
134
+ this.sessions.delete(socket.id);
135
+ }
136
+ });
137
+ }
138
+ handleAudio(socket, frame) {
139
+ const state = this.sessions.get(socket.id);
140
+ if (!state || state.ws.readyState !== WebSocket__default.default.OPEN) return;
141
+ if (frame.sessionId && frame.sessionId !== state.sessionId) {
142
+ this.log(socket.id, "ignore stale audio frame");
143
+ return;
144
+ }
145
+ if (state.ended && frame.status === 2) {
146
+ this.log(socket.id, "ignore duplicate final frame");
147
+ return;
148
+ }
149
+ if (!state.firstFrameSent && frame.status === 2) {
150
+ this.log(socket.id, "ignore lonely final frame");
151
+ return;
152
+ }
153
+ const isFirst = !state.firstFrameSent && frame.status !== 2;
154
+ const status = isFirst ? 0 : frame.status;
155
+ state.frameCount += 1;
156
+ if (status === 0) state.firstFrameSent = true;
157
+ if (status === 2) state.ended = true;
158
+ this.log(socket.id, "audio frame", {
159
+ status,
160
+ hasAudio: Boolean(frame.audio),
161
+ len: frame.audio?.length ?? 0,
162
+ count: state.frameCount
163
+ });
164
+ const payload = {
165
+ data: { status }
166
+ };
167
+ if (status === 0) {
168
+ payload.common = { app_id: this.appId };
169
+ payload.business = {
170
+ language: frame.language || "zh_cn",
171
+ domain: frame.domain || "iat",
172
+ accent: frame.accent || "mandarin",
173
+ vad_eos: 2e3
174
+ };
175
+ payload.data = {
176
+ status: 0,
177
+ format: "audio/L16;rate=16000",
178
+ encoding: "raw",
179
+ audio: frame.audio || ""
180
+ };
181
+ } else if (status === 1) {
182
+ payload.data = {
183
+ status: 1,
184
+ format: "audio/L16;rate=16000",
185
+ encoding: "raw",
186
+ audio: frame.audio || ""
187
+ };
188
+ } else {
189
+ payload.data = { status: 2 };
190
+ }
191
+ state.ws.send(JSON.stringify(payload));
192
+ }
193
+ handleStop(socket, payload) {
194
+ const state = this.sessions.get(socket.id);
195
+ if (payload?.sessionId && state?.sessionId && payload.sessionId !== state.sessionId) {
196
+ this.log(socket.id, "ignore stale stop signal");
197
+ return;
198
+ }
199
+ this.log(socket.id, "stop signal");
200
+ if (state) {
201
+ this.closeUpstream(state);
202
+ this.sessions.delete(socket.id);
203
+ }
204
+ }
205
+ handleDisconnect(socket) {
206
+ const state = this.sessions.get(socket.id);
207
+ if (state) {
208
+ this.closeUpstream(state);
209
+ this.sessions.delete(socket.id);
210
+ }
211
+ }
212
+ // ─── 工具方法 ───
213
+ buildWsUrl() {
214
+ const date = (/* @__PURE__ */ new Date()).toUTCString();
215
+ const requestLine = `GET ${this.path} HTTP/1.1`;
216
+ const signatureOrigin = `host: ${this.host}
217
+ date: ${date}
218
+ ${requestLine}`;
219
+ const signatureSha = crypto__default.default.createHmac("sha256", this.apiSecret).update(signatureOrigin).digest("base64");
220
+ const authOrigin = `api_key="${this.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signatureSha}"`;
221
+ const authorization = Buffer.from(authOrigin).toString("base64");
222
+ const params = new URLSearchParams({ authorization, date, host: this.host });
223
+ return `wss://${this.host}${this.path}?${params.toString()}`;
224
+ }
225
+ extractText(result) {
226
+ const ws = result?.ws;
227
+ if (!Array.isArray(ws)) return "";
228
+ return ws.map((item) => item?.cw?.[0]?.w ?? "").join("").trim();
229
+ }
230
+ extractResult(result) {
231
+ const text = this.extractText(result);
232
+ const pgs = typeof result?.pgs === "string" ? result.pgs : void 0;
233
+ const rgRaw = Array.isArray(result?.rg) ? result.rg : void 0;
234
+ const rg = rgRaw && rgRaw.length === 2 ? [Number(rgRaw[0]), Number(rgRaw[1])] : void 0;
235
+ const sn = typeof result?.sn === "number" ? result.sn : Number.isFinite(Number(result?.sn)) ? Number(result?.sn) : void 0;
236
+ return { text, pgs, rg, sn };
237
+ }
238
+ mergeResult(state, result) {
239
+ const text = result.text?.trim() ?? "";
240
+ const segments = state.resultSegments;
241
+ if (typeof result.sn === "number") {
242
+ if (result.pgs === "rpl" && result.rg) {
243
+ const [start, end] = result.rg;
244
+ const before = segments.slice(0, start);
245
+ const after = segments.slice(end + 1);
246
+ state.resultSegments = [...before, text, ...after];
247
+ } else {
248
+ segments[result.sn] = text;
249
+ state.resultSegments = segments;
250
+ }
251
+ } else if (result.pgs === "rpl" && result.rg) {
252
+ const [start, end] = result.rg;
253
+ const before = segments.slice(0, start);
254
+ const after = segments.slice(end + 1);
255
+ state.resultSegments = [...before, text, ...after];
256
+ } else if (text) {
257
+ segments.push(text);
258
+ state.resultSegments = segments;
259
+ }
260
+ state.resultText = state.resultSegments.filter(Boolean).join("");
261
+ return state.resultText;
262
+ }
263
+ closeUpstream(state) {
264
+ try {
265
+ state.ws.close();
266
+ } catch {
267
+ }
268
+ }
269
+ log(socketId, msg, extra) {
270
+ if (!this.debug) return;
271
+ if (extra) {
272
+ console.log(`[sa2kit/iflytek][${socketId}] ${msg}`, extra);
273
+ } else {
274
+ console.log(`[sa2kit/iflytek][${socketId}] ${msg}`);
275
+ }
276
+ }
277
+ };
278
+
279
+ exports.IflytekServerAdapter = IflytekServerAdapter;
280
+ //# sourceMappingURL=server.js.map
281
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/iflytek/IflytekServerAdapter.ts"],"names":["WebSocket","current","crypto"],"mappings":"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","file":"server.js","sourcesContent":["/**\n * 讯飞语音转文字 — 服务端适配层\n *\n * 运行在 Node.js 上,为每个 Socket.IO 客户端管理一条到讯飞 WebSocket 的上游连接。\n * 纯逻辑,不依赖任何 HTTP 框架。\n *\n * 使用方式:\n * ```ts\n * import { Server } from \"socket.io\";\n * import { IflytekServerAdapter } from \"sa2kit/iflytek/server\";\n *\n * const io = new Server(httpServer, { cors: { origin: \"*\" } });\n * const adapter = new IflytekServerAdapter({\n * appId: process.env.IFLYTEK_APP_ID!,\n * apiKey: process.env.IFLYTEK_API_KEY!,\n * apiSecret: process.env.IFLYTEK_API_SECRET!,\n * });\n *\n * io.on(\"connection\", (socket) => {\n * adapter.attach(socket);\n * });\n * ```\n */\n\nimport crypto from \"crypto\";\nimport WebSocket from \"ws\";\nimport type {\n IflytekServerConfig,\n IflytekAudioFrame,\n IflytekStartPayload,\n IflytekStopPayload,\n} from \"./types\";\n\ninterface SessionState {\n ws: WebSocket;\n sessionId: string;\n frameCount: number;\n firstFrameSent: boolean;\n ended: boolean;\n resultText: string;\n resultSegments: string[];\n}\n\ninterface SocketLike {\n id: string;\n emit(event: string, data: unknown): void;\n on(event: string, handler: (...args: any[]) => void): void;\n}\n\nexport class IflytekServerAdapter {\n private appId: string;\n private apiKey: string;\n private apiSecret: string;\n private host: string;\n private path: string;\n private debug: boolean;\n\n private sessions = new Map<string, SessionState>();\n\n constructor(config: IflytekServerConfig) {\n this.appId = config.appId;\n this.apiKey = config.apiKey;\n this.apiSecret = config.apiSecret;\n this.host = config.host ?? \"iat-api.xfyun.cn\";\n this.path = config.path ?? \"/v2/iat\";\n this.debug = config.debug ?? false;\n }\n\n /**\n * 将适配器绑定到一个 Socket.IO socket 连接上。\n * 自动监听 iflytek:start / iflytek:audio / iflytek:stop / disconnect 事件。\n */\n attach(socket: SocketLike): void {\n socket.on(\"iflytek:start\", (payload?: IflytekStartPayload) => {\n this.handleStart(socket, payload);\n });\n\n socket.on(\"iflytek:audio\", (frame: IflytekAudioFrame) => {\n this.handleAudio(socket, frame);\n });\n\n socket.on(\"iflytek:stop\", (payload?: IflytekStopPayload) => {\n this.handleStop(socket, payload);\n });\n\n socket.on(\"disconnect\", () => {\n this.handleDisconnect(socket);\n });\n }\n\n // ─── 事件处理 ───\n\n private handleStart(socket: SocketLike, payload?: IflytekStartPayload) {\n const sessionId =\n payload?.sessionId ??\n `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\n if (!this.appId || !this.apiKey || !this.apiSecret) {\n socket.emit(\"iflytek:error\", {\n sessionId,\n message:\n \"服务端未配置 IFLYTEK_APP_ID / IFLYTEK_API_KEY / IFLYTEK_API_SECRET\",\n });\n return;\n }\n\n const existing = this.sessions.get(socket.id);\n if (existing) {\n if (existing.ended) {\n this.log(socket.id, \"replace ended session\");\n this.closeUpstream(existing);\n this.sessions.delete(socket.id);\n } else if (existing.ws.readyState === WebSocket.OPEN) {\n this.log(socket.id, \"duplicate start ignored (session open)\");\n socket.emit(\"iflytek:ready\", { sessionId: existing.sessionId });\n return;\n } else if (existing.ws.readyState === WebSocket.CONNECTING) {\n this.log(socket.id, \"duplicate start ignored (session connecting)\");\n return;\n } else {\n this.sessions.delete(socket.id);\n }\n }\n\n const wsUrl = this.buildWsUrl();\n const ws = new WebSocket(wsUrl);\n const state: SessionState = {\n ws,\n sessionId,\n frameCount: 0,\n firstFrameSent: false,\n ended: false,\n resultText: \"\",\n resultSegments: [],\n };\n this.sessions.set(socket.id, state);\n this.log(socket.id, \"start session\", { sessionId });\n\n ws.on(\"open\", () => {\n this.log(socket.id, \"upstream open\");\n socket.emit(\"iflytek:ready\", { sessionId });\n });\n\n ws.on(\"message\", (raw) => {\n try {\n const msg = JSON.parse(String(raw));\n this.log(socket.id, \"upstream message\", {\n code: msg.code,\n status: msg?.data?.status,\n });\n if (msg.code !== 0) {\n socket.emit(\"iflytek:error\", {\n sessionId,\n message: msg.message || `讯飞返回错误 code=${msg.code}`,\n });\n return;\n }\n\n const current = this.sessions.get(socket.id);\n if (!current || current.ws !== ws) return;\n\n const result = this.extractResult(msg.data?.result);\n const mergedText = this.mergeResult(current, result);\n const isFinal = msg.data?.status === 2;\n if (mergedText || isFinal) {\n socket.emit(\"iflytek:result\", {\n sessionId,\n text: mergedText,\n isFinal,\n });\n }\n\n if (isFinal) {\n const current = this.sessions.get(socket.id);\n if (current?.ws === ws) {\n this.closeUpstream(current);\n }\n }\n } catch (e: any) {\n socket.emit(\"iflytek:error\", { sessionId, message: e.message });\n }\n });\n\n ws.on(\"error\", (e: any) => {\n if (e?.message?.includes(\"before the connection was established\")) {\n this.log(socket.id, \"ignore early-close error\");\n return;\n }\n socket.emit(\"iflytek:error\", {\n sessionId,\n message: e?.message || \"讯飞 WebSocket 连接失败\",\n });\n });\n\n ws.on(\"close\", () => {\n this.log(socket.id, \"upstream close\");\n const current = this.sessions.get(socket.id);\n if (current?.ws === ws) {\n this.sessions.delete(socket.id);\n }\n });\n }\n\n private handleAudio(socket: SocketLike, frame: IflytekAudioFrame) {\n const state = this.sessions.get(socket.id);\n if (!state || state.ws.readyState !== WebSocket.OPEN) return;\n\n if (frame.sessionId && frame.sessionId !== state.sessionId) {\n this.log(socket.id, \"ignore stale audio frame\");\n return;\n }\n if (state.ended && frame.status === 2) {\n this.log(socket.id, \"ignore duplicate final frame\");\n return;\n }\n if (!state.firstFrameSent && frame.status === 2) {\n this.log(socket.id, \"ignore lonely final frame\");\n return;\n }\n\n const isFirst = !state.firstFrameSent && frame.status !== 2;\n const status: 0 | 1 | 2 = isFirst ? 0 : frame.status;\n\n state.frameCount += 1;\n if (status === 0) state.firstFrameSent = true;\n if (status === 2) state.ended = true;\n\n this.log(socket.id, \"audio frame\", {\n status,\n hasAudio: Boolean(frame.audio),\n len: frame.audio?.length ?? 0,\n count: state.frameCount,\n });\n\n const payload: Record<string, unknown> = {\n data: { status },\n };\n\n if (status === 0) {\n payload.common = { app_id: this.appId };\n payload.business = {\n language: frame.language || \"zh_cn\",\n domain: frame.domain || \"iat\",\n accent: frame.accent || \"mandarin\",\n vad_eos: 2000,\n };\n payload.data = {\n status: 0,\n format: \"audio/L16;rate=16000\",\n encoding: \"raw\",\n audio: frame.audio || \"\",\n };\n } else if (status === 1) {\n payload.data = {\n status: 1,\n format: \"audio/L16;rate=16000\",\n encoding: \"raw\",\n audio: frame.audio || \"\",\n };\n } else {\n payload.data = { status: 2 };\n }\n\n state.ws.send(JSON.stringify(payload));\n }\n\n private handleStop(socket: SocketLike, payload?: IflytekStopPayload) {\n const state = this.sessions.get(socket.id);\n if (\n payload?.sessionId &&\n state?.sessionId &&\n payload.sessionId !== state.sessionId\n ) {\n this.log(socket.id, \"ignore stale stop signal\");\n return;\n }\n this.log(socket.id, \"stop signal\");\n if (state) {\n this.closeUpstream(state);\n this.sessions.delete(socket.id);\n }\n }\n\n private handleDisconnect(socket: SocketLike) {\n const state = this.sessions.get(socket.id);\n if (state) {\n this.closeUpstream(state);\n this.sessions.delete(socket.id);\n }\n }\n\n // ─── 工具方法 ───\n\n private buildWsUrl(): string {\n const date = new Date().toUTCString();\n const requestLine = `GET ${this.path} HTTP/1.1`;\n const signatureOrigin = `host: ${this.host}\\ndate: ${date}\\n${requestLine}`;\n const signatureSha = crypto\n .createHmac(\"sha256\", this.apiSecret)\n .update(signatureOrigin)\n .digest(\"base64\");\n const authOrigin = `api_key=\"${this.apiKey}\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"${signatureSha}\"`;\n const authorization = Buffer.from(authOrigin).toString(\"base64\");\n const params = new URLSearchParams({ authorization, date, host: this.host });\n return `wss://${this.host}${this.path}?${params.toString()}`;\n }\n\n private extractText(result: any): string {\n const ws = result?.ws;\n if (!Array.isArray(ws)) return \"\";\n return ws\n .map((item: any) => item?.cw?.[0]?.w ?? \"\")\n .join(\"\")\n .trim();\n }\n\n private extractResult(result: any): {\n text: string;\n pgs?: string;\n rg?: [number, number];\n sn?: number;\n } {\n const text = this.extractText(result);\n const pgs =\n typeof result?.pgs === \"string\" ? result.pgs : undefined;\n const rgRaw = Array.isArray(result?.rg) ? result.rg : undefined;\n const rg =\n rgRaw && rgRaw.length === 2\n ? ([Number(rgRaw[0]), Number(rgRaw[1])] as [number, number])\n : undefined;\n const sn =\n typeof result?.sn === \"number\"\n ? result.sn\n : Number.isFinite(Number(result?.sn))\n ? Number(result?.sn)\n : undefined;\n return { text, pgs, rg, sn };\n }\n\n private mergeResult(\n state: SessionState,\n result: { text: string; pgs?: string; rg?: [number, number]; sn?: number },\n ): string {\n const text = result.text?.trim() ?? \"\";\n const segments = state.resultSegments;\n\n if (typeof result.sn === \"number\") {\n if (result.pgs === \"rpl\" && result.rg) {\n const [start, end] = result.rg;\n const before = segments.slice(0, start);\n const after = segments.slice(end + 1);\n state.resultSegments = [...before, text, ...after];\n } else {\n segments[result.sn] = text;\n state.resultSegments = segments;\n }\n } else if (result.pgs === \"rpl\" && result.rg) {\n const [start, end] = result.rg;\n const before = segments.slice(0, start);\n const after = segments.slice(end + 1);\n state.resultSegments = [...before, text, ...after];\n } else if (text) {\n segments.push(text);\n state.resultSegments = segments;\n }\n\n state.resultText = state.resultSegments.filter(Boolean).join(\"\");\n return state.resultText;\n }\n\n private closeUpstream(state: SessionState) {\n try {\n state.ws.close();\n } catch {}\n }\n\n private log(socketId: string, msg: string, extra?: Record<string, unknown>) {\n if (!this.debug) return;\n if (extra) {\n console.log(`[sa2kit/iflytek][${socketId}] ${msg}`, extra);\n } else {\n console.log(`[sa2kit/iflytek][${socketId}] ${msg}`);\n }\n }\n}\n"]}
@@ -0,0 +1,274 @@
1
+ import '../chunk-WMJKH4XE.mjs';
2
+ import crypto from 'crypto';
3
+ import WebSocket from 'ws';
4
+
5
+ var IflytekServerAdapter = class {
6
+ constructor(config) {
7
+ this.sessions = /* @__PURE__ */ new Map();
8
+ this.appId = config.appId;
9
+ this.apiKey = config.apiKey;
10
+ this.apiSecret = config.apiSecret;
11
+ this.host = config.host ?? "iat-api.xfyun.cn";
12
+ this.path = config.path ?? "/v2/iat";
13
+ this.debug = config.debug ?? false;
14
+ }
15
+ /**
16
+ * 将适配器绑定到一个 Socket.IO socket 连接上。
17
+ * 自动监听 iflytek:start / iflytek:audio / iflytek:stop / disconnect 事件。
18
+ */
19
+ attach(socket) {
20
+ socket.on("iflytek:start", (payload) => {
21
+ this.handleStart(socket, payload);
22
+ });
23
+ socket.on("iflytek:audio", (frame) => {
24
+ this.handleAudio(socket, frame);
25
+ });
26
+ socket.on("iflytek:stop", (payload) => {
27
+ this.handleStop(socket, payload);
28
+ });
29
+ socket.on("disconnect", () => {
30
+ this.handleDisconnect(socket);
31
+ });
32
+ }
33
+ // ─── 事件处理 ───
34
+ handleStart(socket, payload) {
35
+ const sessionId = payload?.sessionId ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
36
+ if (!this.appId || !this.apiKey || !this.apiSecret) {
37
+ socket.emit("iflytek:error", {
38
+ sessionId,
39
+ message: "\u670D\u52A1\u7AEF\u672A\u914D\u7F6E IFLYTEK_APP_ID / IFLYTEK_API_KEY / IFLYTEK_API_SECRET"
40
+ });
41
+ return;
42
+ }
43
+ const existing = this.sessions.get(socket.id);
44
+ if (existing) {
45
+ if (existing.ended) {
46
+ this.log(socket.id, "replace ended session");
47
+ this.closeUpstream(existing);
48
+ this.sessions.delete(socket.id);
49
+ } else if (existing.ws.readyState === WebSocket.OPEN) {
50
+ this.log(socket.id, "duplicate start ignored (session open)");
51
+ socket.emit("iflytek:ready", { sessionId: existing.sessionId });
52
+ return;
53
+ } else if (existing.ws.readyState === WebSocket.CONNECTING) {
54
+ this.log(socket.id, "duplicate start ignored (session connecting)");
55
+ return;
56
+ } else {
57
+ this.sessions.delete(socket.id);
58
+ }
59
+ }
60
+ const wsUrl = this.buildWsUrl();
61
+ const ws = new WebSocket(wsUrl);
62
+ const state = {
63
+ ws,
64
+ sessionId,
65
+ frameCount: 0,
66
+ firstFrameSent: false,
67
+ ended: false,
68
+ resultText: "",
69
+ resultSegments: []
70
+ };
71
+ this.sessions.set(socket.id, state);
72
+ this.log(socket.id, "start session", { sessionId });
73
+ ws.on("open", () => {
74
+ this.log(socket.id, "upstream open");
75
+ socket.emit("iflytek:ready", { sessionId });
76
+ });
77
+ ws.on("message", (raw) => {
78
+ try {
79
+ const msg = JSON.parse(String(raw));
80
+ this.log(socket.id, "upstream message", {
81
+ code: msg.code,
82
+ status: msg?.data?.status
83
+ });
84
+ if (msg.code !== 0) {
85
+ socket.emit("iflytek:error", {
86
+ sessionId,
87
+ message: msg.message || `\u8BAF\u98DE\u8FD4\u56DE\u9519\u8BEF code=${msg.code}`
88
+ });
89
+ return;
90
+ }
91
+ const current = this.sessions.get(socket.id);
92
+ if (!current || current.ws !== ws) return;
93
+ const result = this.extractResult(msg.data?.result);
94
+ const mergedText = this.mergeResult(current, result);
95
+ const isFinal = msg.data?.status === 2;
96
+ if (mergedText || isFinal) {
97
+ socket.emit("iflytek:result", {
98
+ sessionId,
99
+ text: mergedText,
100
+ isFinal
101
+ });
102
+ }
103
+ if (isFinal) {
104
+ const current2 = this.sessions.get(socket.id);
105
+ if (current2?.ws === ws) {
106
+ this.closeUpstream(current2);
107
+ }
108
+ }
109
+ } catch (e) {
110
+ socket.emit("iflytek:error", { sessionId, message: e.message });
111
+ }
112
+ });
113
+ ws.on("error", (e) => {
114
+ if (e?.message?.includes("before the connection was established")) {
115
+ this.log(socket.id, "ignore early-close error");
116
+ return;
117
+ }
118
+ socket.emit("iflytek:error", {
119
+ sessionId,
120
+ message: e?.message || "\u8BAF\u98DE WebSocket \u8FDE\u63A5\u5931\u8D25"
121
+ });
122
+ });
123
+ ws.on("close", () => {
124
+ this.log(socket.id, "upstream close");
125
+ const current = this.sessions.get(socket.id);
126
+ if (current?.ws === ws) {
127
+ this.sessions.delete(socket.id);
128
+ }
129
+ });
130
+ }
131
+ handleAudio(socket, frame) {
132
+ const state = this.sessions.get(socket.id);
133
+ if (!state || state.ws.readyState !== WebSocket.OPEN) return;
134
+ if (frame.sessionId && frame.sessionId !== state.sessionId) {
135
+ this.log(socket.id, "ignore stale audio frame");
136
+ return;
137
+ }
138
+ if (state.ended && frame.status === 2) {
139
+ this.log(socket.id, "ignore duplicate final frame");
140
+ return;
141
+ }
142
+ if (!state.firstFrameSent && frame.status === 2) {
143
+ this.log(socket.id, "ignore lonely final frame");
144
+ return;
145
+ }
146
+ const isFirst = !state.firstFrameSent && frame.status !== 2;
147
+ const status = isFirst ? 0 : frame.status;
148
+ state.frameCount += 1;
149
+ if (status === 0) state.firstFrameSent = true;
150
+ if (status === 2) state.ended = true;
151
+ this.log(socket.id, "audio frame", {
152
+ status,
153
+ hasAudio: Boolean(frame.audio),
154
+ len: frame.audio?.length ?? 0,
155
+ count: state.frameCount
156
+ });
157
+ const payload = {
158
+ data: { status }
159
+ };
160
+ if (status === 0) {
161
+ payload.common = { app_id: this.appId };
162
+ payload.business = {
163
+ language: frame.language || "zh_cn",
164
+ domain: frame.domain || "iat",
165
+ accent: frame.accent || "mandarin",
166
+ vad_eos: 2e3
167
+ };
168
+ payload.data = {
169
+ status: 0,
170
+ format: "audio/L16;rate=16000",
171
+ encoding: "raw",
172
+ audio: frame.audio || ""
173
+ };
174
+ } else if (status === 1) {
175
+ payload.data = {
176
+ status: 1,
177
+ format: "audio/L16;rate=16000",
178
+ encoding: "raw",
179
+ audio: frame.audio || ""
180
+ };
181
+ } else {
182
+ payload.data = { status: 2 };
183
+ }
184
+ state.ws.send(JSON.stringify(payload));
185
+ }
186
+ handleStop(socket, payload) {
187
+ const state = this.sessions.get(socket.id);
188
+ if (payload?.sessionId && state?.sessionId && payload.sessionId !== state.sessionId) {
189
+ this.log(socket.id, "ignore stale stop signal");
190
+ return;
191
+ }
192
+ this.log(socket.id, "stop signal");
193
+ if (state) {
194
+ this.closeUpstream(state);
195
+ this.sessions.delete(socket.id);
196
+ }
197
+ }
198
+ handleDisconnect(socket) {
199
+ const state = this.sessions.get(socket.id);
200
+ if (state) {
201
+ this.closeUpstream(state);
202
+ this.sessions.delete(socket.id);
203
+ }
204
+ }
205
+ // ─── 工具方法 ───
206
+ buildWsUrl() {
207
+ const date = (/* @__PURE__ */ new Date()).toUTCString();
208
+ const requestLine = `GET ${this.path} HTTP/1.1`;
209
+ const signatureOrigin = `host: ${this.host}
210
+ date: ${date}
211
+ ${requestLine}`;
212
+ const signatureSha = crypto.createHmac("sha256", this.apiSecret).update(signatureOrigin).digest("base64");
213
+ const authOrigin = `api_key="${this.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signatureSha}"`;
214
+ const authorization = Buffer.from(authOrigin).toString("base64");
215
+ const params = new URLSearchParams({ authorization, date, host: this.host });
216
+ return `wss://${this.host}${this.path}?${params.toString()}`;
217
+ }
218
+ extractText(result) {
219
+ const ws = result?.ws;
220
+ if (!Array.isArray(ws)) return "";
221
+ return ws.map((item) => item?.cw?.[0]?.w ?? "").join("").trim();
222
+ }
223
+ extractResult(result) {
224
+ const text = this.extractText(result);
225
+ const pgs = typeof result?.pgs === "string" ? result.pgs : void 0;
226
+ const rgRaw = Array.isArray(result?.rg) ? result.rg : void 0;
227
+ const rg = rgRaw && rgRaw.length === 2 ? [Number(rgRaw[0]), Number(rgRaw[1])] : void 0;
228
+ const sn = typeof result?.sn === "number" ? result.sn : Number.isFinite(Number(result?.sn)) ? Number(result?.sn) : void 0;
229
+ return { text, pgs, rg, sn };
230
+ }
231
+ mergeResult(state, result) {
232
+ const text = result.text?.trim() ?? "";
233
+ const segments = state.resultSegments;
234
+ if (typeof result.sn === "number") {
235
+ if (result.pgs === "rpl" && result.rg) {
236
+ const [start, end] = result.rg;
237
+ const before = segments.slice(0, start);
238
+ const after = segments.slice(end + 1);
239
+ state.resultSegments = [...before, text, ...after];
240
+ } else {
241
+ segments[result.sn] = text;
242
+ state.resultSegments = segments;
243
+ }
244
+ } else if (result.pgs === "rpl" && result.rg) {
245
+ const [start, end] = result.rg;
246
+ const before = segments.slice(0, start);
247
+ const after = segments.slice(end + 1);
248
+ state.resultSegments = [...before, text, ...after];
249
+ } else if (text) {
250
+ segments.push(text);
251
+ state.resultSegments = segments;
252
+ }
253
+ state.resultText = state.resultSegments.filter(Boolean).join("");
254
+ return state.resultText;
255
+ }
256
+ closeUpstream(state) {
257
+ try {
258
+ state.ws.close();
259
+ } catch {
260
+ }
261
+ }
262
+ log(socketId, msg, extra) {
263
+ if (!this.debug) return;
264
+ if (extra) {
265
+ console.log(`[sa2kit/iflytek][${socketId}] ${msg}`, extra);
266
+ } else {
267
+ console.log(`[sa2kit/iflytek][${socketId}] ${msg}`);
268
+ }
269
+ }
270
+ };
271
+
272
+ export { IflytekServerAdapter };
273
+ //# sourceMappingURL=server.mjs.map
274
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/iflytek/IflytekServerAdapter.ts"],"names":["current"],"mappings":"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","file":"server.mjs","sourcesContent":["/**\n * 讯飞语音转文字 — 服务端适配层\n *\n * 运行在 Node.js 上,为每个 Socket.IO 客户端管理一条到讯飞 WebSocket 的上游连接。\n * 纯逻辑,不依赖任何 HTTP 框架。\n *\n * 使用方式:\n * ```ts\n * import { Server } from \"socket.io\";\n * import { IflytekServerAdapter } from \"sa2kit/iflytek/server\";\n *\n * const io = new Server(httpServer, { cors: { origin: \"*\" } });\n * const adapter = new IflytekServerAdapter({\n * appId: process.env.IFLYTEK_APP_ID!,\n * apiKey: process.env.IFLYTEK_API_KEY!,\n * apiSecret: process.env.IFLYTEK_API_SECRET!,\n * });\n *\n * io.on(\"connection\", (socket) => {\n * adapter.attach(socket);\n * });\n * ```\n */\n\nimport crypto from \"crypto\";\nimport WebSocket from \"ws\";\nimport type {\n IflytekServerConfig,\n IflytekAudioFrame,\n IflytekStartPayload,\n IflytekStopPayload,\n} from \"./types\";\n\ninterface SessionState {\n ws: WebSocket;\n sessionId: string;\n frameCount: number;\n firstFrameSent: boolean;\n ended: boolean;\n resultText: string;\n resultSegments: string[];\n}\n\ninterface SocketLike {\n id: string;\n emit(event: string, data: unknown): void;\n on(event: string, handler: (...args: any[]) => void): void;\n}\n\nexport class IflytekServerAdapter {\n private appId: string;\n private apiKey: string;\n private apiSecret: string;\n private host: string;\n private path: string;\n private debug: boolean;\n\n private sessions = new Map<string, SessionState>();\n\n constructor(config: IflytekServerConfig) {\n this.appId = config.appId;\n this.apiKey = config.apiKey;\n this.apiSecret = config.apiSecret;\n this.host = config.host ?? \"iat-api.xfyun.cn\";\n this.path = config.path ?? \"/v2/iat\";\n this.debug = config.debug ?? false;\n }\n\n /**\n * 将适配器绑定到一个 Socket.IO socket 连接上。\n * 自动监听 iflytek:start / iflytek:audio / iflytek:stop / disconnect 事件。\n */\n attach(socket: SocketLike): void {\n socket.on(\"iflytek:start\", (payload?: IflytekStartPayload) => {\n this.handleStart(socket, payload);\n });\n\n socket.on(\"iflytek:audio\", (frame: IflytekAudioFrame) => {\n this.handleAudio(socket, frame);\n });\n\n socket.on(\"iflytek:stop\", (payload?: IflytekStopPayload) => {\n this.handleStop(socket, payload);\n });\n\n socket.on(\"disconnect\", () => {\n this.handleDisconnect(socket);\n });\n }\n\n // ─── 事件处理 ───\n\n private handleStart(socket: SocketLike, payload?: IflytekStartPayload) {\n const sessionId =\n payload?.sessionId ??\n `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\n if (!this.appId || !this.apiKey || !this.apiSecret) {\n socket.emit(\"iflytek:error\", {\n sessionId,\n message:\n \"服务端未配置 IFLYTEK_APP_ID / IFLYTEK_API_KEY / IFLYTEK_API_SECRET\",\n });\n return;\n }\n\n const existing = this.sessions.get(socket.id);\n if (existing) {\n if (existing.ended) {\n this.log(socket.id, \"replace ended session\");\n this.closeUpstream(existing);\n this.sessions.delete(socket.id);\n } else if (existing.ws.readyState === WebSocket.OPEN) {\n this.log(socket.id, \"duplicate start ignored (session open)\");\n socket.emit(\"iflytek:ready\", { sessionId: existing.sessionId });\n return;\n } else if (existing.ws.readyState === WebSocket.CONNECTING) {\n this.log(socket.id, \"duplicate start ignored (session connecting)\");\n return;\n } else {\n this.sessions.delete(socket.id);\n }\n }\n\n const wsUrl = this.buildWsUrl();\n const ws = new WebSocket(wsUrl);\n const state: SessionState = {\n ws,\n sessionId,\n frameCount: 0,\n firstFrameSent: false,\n ended: false,\n resultText: \"\",\n resultSegments: [],\n };\n this.sessions.set(socket.id, state);\n this.log(socket.id, \"start session\", { sessionId });\n\n ws.on(\"open\", () => {\n this.log(socket.id, \"upstream open\");\n socket.emit(\"iflytek:ready\", { sessionId });\n });\n\n ws.on(\"message\", (raw) => {\n try {\n const msg = JSON.parse(String(raw));\n this.log(socket.id, \"upstream message\", {\n code: msg.code,\n status: msg?.data?.status,\n });\n if (msg.code !== 0) {\n socket.emit(\"iflytek:error\", {\n sessionId,\n message: msg.message || `讯飞返回错误 code=${msg.code}`,\n });\n return;\n }\n\n const current = this.sessions.get(socket.id);\n if (!current || current.ws !== ws) return;\n\n const result = this.extractResult(msg.data?.result);\n const mergedText = this.mergeResult(current, result);\n const isFinal = msg.data?.status === 2;\n if (mergedText || isFinal) {\n socket.emit(\"iflytek:result\", {\n sessionId,\n text: mergedText,\n isFinal,\n });\n }\n\n if (isFinal) {\n const current = this.sessions.get(socket.id);\n if (current?.ws === ws) {\n this.closeUpstream(current);\n }\n }\n } catch (e: any) {\n socket.emit(\"iflytek:error\", { sessionId, message: e.message });\n }\n });\n\n ws.on(\"error\", (e: any) => {\n if (e?.message?.includes(\"before the connection was established\")) {\n this.log(socket.id, \"ignore early-close error\");\n return;\n }\n socket.emit(\"iflytek:error\", {\n sessionId,\n message: e?.message || \"讯飞 WebSocket 连接失败\",\n });\n });\n\n ws.on(\"close\", () => {\n this.log(socket.id, \"upstream close\");\n const current = this.sessions.get(socket.id);\n if (current?.ws === ws) {\n this.sessions.delete(socket.id);\n }\n });\n }\n\n private handleAudio(socket: SocketLike, frame: IflytekAudioFrame) {\n const state = this.sessions.get(socket.id);\n if (!state || state.ws.readyState !== WebSocket.OPEN) return;\n\n if (frame.sessionId && frame.sessionId !== state.sessionId) {\n this.log(socket.id, \"ignore stale audio frame\");\n return;\n }\n if (state.ended && frame.status === 2) {\n this.log(socket.id, \"ignore duplicate final frame\");\n return;\n }\n if (!state.firstFrameSent && frame.status === 2) {\n this.log(socket.id, \"ignore lonely final frame\");\n return;\n }\n\n const isFirst = !state.firstFrameSent && frame.status !== 2;\n const status: 0 | 1 | 2 = isFirst ? 0 : frame.status;\n\n state.frameCount += 1;\n if (status === 0) state.firstFrameSent = true;\n if (status === 2) state.ended = true;\n\n this.log(socket.id, \"audio frame\", {\n status,\n hasAudio: Boolean(frame.audio),\n len: frame.audio?.length ?? 0,\n count: state.frameCount,\n });\n\n const payload: Record<string, unknown> = {\n data: { status },\n };\n\n if (status === 0) {\n payload.common = { app_id: this.appId };\n payload.business = {\n language: frame.language || \"zh_cn\",\n domain: frame.domain || \"iat\",\n accent: frame.accent || \"mandarin\",\n vad_eos: 2000,\n };\n payload.data = {\n status: 0,\n format: \"audio/L16;rate=16000\",\n encoding: \"raw\",\n audio: frame.audio || \"\",\n };\n } else if (status === 1) {\n payload.data = {\n status: 1,\n format: \"audio/L16;rate=16000\",\n encoding: \"raw\",\n audio: frame.audio || \"\",\n };\n } else {\n payload.data = { status: 2 };\n }\n\n state.ws.send(JSON.stringify(payload));\n }\n\n private handleStop(socket: SocketLike, payload?: IflytekStopPayload) {\n const state = this.sessions.get(socket.id);\n if (\n payload?.sessionId &&\n state?.sessionId &&\n payload.sessionId !== state.sessionId\n ) {\n this.log(socket.id, \"ignore stale stop signal\");\n return;\n }\n this.log(socket.id, \"stop signal\");\n if (state) {\n this.closeUpstream(state);\n this.sessions.delete(socket.id);\n }\n }\n\n private handleDisconnect(socket: SocketLike) {\n const state = this.sessions.get(socket.id);\n if (state) {\n this.closeUpstream(state);\n this.sessions.delete(socket.id);\n }\n }\n\n // ─── 工具方法 ───\n\n private buildWsUrl(): string {\n const date = new Date().toUTCString();\n const requestLine = `GET ${this.path} HTTP/1.1`;\n const signatureOrigin = `host: ${this.host}\\ndate: ${date}\\n${requestLine}`;\n const signatureSha = crypto\n .createHmac(\"sha256\", this.apiSecret)\n .update(signatureOrigin)\n .digest(\"base64\");\n const authOrigin = `api_key=\"${this.apiKey}\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"${signatureSha}\"`;\n const authorization = Buffer.from(authOrigin).toString(\"base64\");\n const params = new URLSearchParams({ authorization, date, host: this.host });\n return `wss://${this.host}${this.path}?${params.toString()}`;\n }\n\n private extractText(result: any): string {\n const ws = result?.ws;\n if (!Array.isArray(ws)) return \"\";\n return ws\n .map((item: any) => item?.cw?.[0]?.w ?? \"\")\n .join(\"\")\n .trim();\n }\n\n private extractResult(result: any): {\n text: string;\n pgs?: string;\n rg?: [number, number];\n sn?: number;\n } {\n const text = this.extractText(result);\n const pgs =\n typeof result?.pgs === \"string\" ? result.pgs : undefined;\n const rgRaw = Array.isArray(result?.rg) ? result.rg : undefined;\n const rg =\n rgRaw && rgRaw.length === 2\n ? ([Number(rgRaw[0]), Number(rgRaw[1])] as [number, number])\n : undefined;\n const sn =\n typeof result?.sn === \"number\"\n ? result.sn\n : Number.isFinite(Number(result?.sn))\n ? Number(result?.sn)\n : undefined;\n return { text, pgs, rg, sn };\n }\n\n private mergeResult(\n state: SessionState,\n result: { text: string; pgs?: string; rg?: [number, number]; sn?: number },\n ): string {\n const text = result.text?.trim() ?? \"\";\n const segments = state.resultSegments;\n\n if (typeof result.sn === \"number\") {\n if (result.pgs === \"rpl\" && result.rg) {\n const [start, end] = result.rg;\n const before = segments.slice(0, start);\n const after = segments.slice(end + 1);\n state.resultSegments = [...before, text, ...after];\n } else {\n segments[result.sn] = text;\n state.resultSegments = segments;\n }\n } else if (result.pgs === \"rpl\" && result.rg) {\n const [start, end] = result.rg;\n const before = segments.slice(0, start);\n const after = segments.slice(end + 1);\n state.resultSegments = [...before, text, ...after];\n } else if (text) {\n segments.push(text);\n state.resultSegments = segments;\n }\n\n state.resultText = state.resultSegments.filter(Boolean).join(\"\");\n return state.resultText;\n }\n\n private closeUpstream(state: SessionState) {\n try {\n state.ws.close();\n } catch {}\n }\n\n private log(socketId: string, msg: string, extra?: Record<string, unknown>) {\n if (!this.debug) return;\n if (extra) {\n console.log(`[sa2kit/iflytek][${socketId}] ${msg}`, extra);\n } else {\n console.log(`[sa2kit/iflytek][${socketId}] ${msg}`);\n }\n }\n}\n"]}