sa2kit 1.6.50 → 1.6.57

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.
Files changed (101) hide show
  1. package/dist/booking-BXtkG2ns.d.mts +386 -0
  2. package/dist/booking-BXtkG2ns.d.ts +386 -0
  3. package/dist/bookingAdminService-B2QDjSHJ.d.ts +193 -0
  4. package/dist/bookingAdminService-Dmg7dC6V.d.mts +193 -0
  5. package/dist/cart-CcZ7rQyG.d.mts +176 -0
  6. package/dist/cart-D3o67Q3H.d.ts +176 -0
  7. package/dist/chunk-53WLQ22S.js +119 -0
  8. package/dist/chunk-53WLQ22S.js.map +1 -0
  9. package/dist/{chunk-SHY424RZ.mjs → chunk-6NMIKAE7.mjs} +3 -3
  10. package/dist/{chunk-SHY424RZ.mjs.map → chunk-6NMIKAE7.mjs.map} +1 -1
  11. package/dist/chunk-ANKVXRPY.mjs +30 -0
  12. package/dist/chunk-ANKVXRPY.mjs.map +1 -0
  13. package/dist/{chunk-IZOIVYOW.js → chunk-DENROXAE.js} +2 -2
  14. package/dist/{chunk-IZOIVYOW.js.map → chunk-DENROXAE.js.map} +1 -1
  15. package/dist/chunk-HYZ5C6FY.mjs +3522 -0
  16. package/dist/chunk-HYZ5C6FY.mjs.map +1 -0
  17. package/dist/{chunk-7JN25DJB.js → chunk-IPY26RQH.js} +11 -11
  18. package/dist/{chunk-7JN25DJB.js.map → chunk-IPY26RQH.js.map} +1 -1
  19. package/dist/{chunk-GFVAIT6Y.mjs → chunk-MFG2Y6UR.mjs} +2 -2
  20. package/dist/{chunk-GFVAIT6Y.mjs.map → chunk-MFG2Y6UR.mjs.map} +1 -1
  21. package/dist/chunk-TJZDPOO7.js +34 -0
  22. package/dist/chunk-TJZDPOO7.js.map +1 -0
  23. package/dist/chunk-U7AQC2Z7.js +3542 -0
  24. package/dist/chunk-U7AQC2Z7.js.map +1 -0
  25. package/dist/chunk-UR5TU4MW.mjs +113 -0
  26. package/dist/chunk-UR5TU4MW.mjs.map +1 -0
  27. package/dist/chunk-V6BXO6ZS.mjs +6107 -0
  28. package/dist/chunk-V6BXO6ZS.mjs.map +1 -0
  29. package/dist/chunk-W2NCOORK.js +6138 -0
  30. package/dist/chunk-W2NCOORK.js.map +1 -0
  31. package/dist/iflytek/index.d.mts +109 -0
  32. package/dist/iflytek/index.d.ts +109 -0
  33. package/dist/iflytek/index.js +365 -0
  34. package/dist/iflytek/index.js.map +1 -0
  35. package/dist/iflytek/index.mjs +362 -0
  36. package/dist/iflytek/index.mjs.map +1 -0
  37. package/dist/iflytek/server.d.mts +59 -0
  38. package/dist/iflytek/server.d.ts +59 -0
  39. package/dist/iflytek/server.js +281 -0
  40. package/dist/iflytek/server.js.map +1 -0
  41. package/dist/iflytek/server.mjs +274 -0
  42. package/dist/iflytek/server.mjs.map +1 -0
  43. package/dist/showmasterpiece/db/index.d.mts +3028 -0
  44. package/dist/showmasterpiece/db/index.d.ts +3028 -0
  45. package/dist/showmasterpiece/db/index.js +179 -0
  46. package/dist/showmasterpiece/db/index.js.map +1 -0
  47. package/dist/showmasterpiece/db/index.mjs +6 -0
  48. package/dist/showmasterpiece/db/index.mjs.map +1 -0
  49. package/dist/showmasterpiece/index.d.mts +10 -1729
  50. package/dist/showmasterpiece/index.d.ts +10 -1729
  51. package/dist/showmasterpiece/index.js +435 -9654
  52. package/dist/showmasterpiece/index.js.map +1 -1
  53. package/dist/showmasterpiece/index.mjs +9 -9628
  54. package/dist/showmasterpiece/index.mjs.map +1 -1
  55. package/dist/showmasterpiece/logic/index.d.mts +372 -0
  56. package/dist/showmasterpiece/logic/index.d.ts +372 -0
  57. package/dist/showmasterpiece/logic/index.js +91 -0
  58. package/dist/showmasterpiece/logic/index.js.map +1 -0
  59. package/dist/showmasterpiece/logic/index.mjs +6 -0
  60. package/dist/showmasterpiece/logic/index.mjs.map +1 -0
  61. package/dist/showmasterpiece/server/index.d.mts +3 -2704
  62. package/dist/showmasterpiece/server/index.d.ts +3 -2704
  63. package/dist/showmasterpiece/server/index.js +43 -43
  64. package/dist/showmasterpiece/server/index.mjs +2 -2
  65. package/dist/showmasterpiece/ui/miniapp/index.d.mts +86 -0
  66. package/dist/showmasterpiece/ui/miniapp/index.d.ts +86 -0
  67. package/dist/showmasterpiece/ui/miniapp/index.js +196 -0
  68. package/dist/showmasterpiece/ui/miniapp/index.js.map +1 -0
  69. package/dist/showmasterpiece/ui/miniapp/index.mjs +183 -0
  70. package/dist/showmasterpiece/ui/miniapp/index.mjs.map +1 -0
  71. package/dist/showmasterpiece/ui/web/index.d.mts +600 -0
  72. package/dist/showmasterpiece/ui/web/index.d.ts +600 -0
  73. package/dist/showmasterpiece/ui/web/index.js +131 -0
  74. package/dist/showmasterpiece/ui/web/index.js.map +1 -0
  75. package/dist/showmasterpiece/ui/web/index.mjs +14 -0
  76. package/dist/showmasterpiece/ui/web/index.mjs.map +1 -0
  77. package/dist/types-C4bbgHHW.d.mts +100 -0
  78. package/dist/types-C4bbgHHW.d.ts +100 -0
  79. package/dist/universalFile/server/index.js +5882 -288
  80. package/dist/universalFile/server/index.js.map +1 -1
  81. package/dist/universalFile/server/index.mjs +5796 -5
  82. package/dist/universalFile/server/index.mjs.map +1 -1
  83. package/package.json +45 -4
  84. package/dist/chunk-QROLPPXP.mjs +0 -5797
  85. package/dist/chunk-QROLPPXP.mjs.map +0 -1
  86. package/dist/chunk-VLZ5N6XZ.js +0 -5888
  87. package/dist/chunk-VLZ5N6XZ.js.map +0 -1
  88. package/dist/popupConfig-BznThU1O.d.mts +0 -330
  89. package/dist/popupConfig-BznThU1O.d.ts +0 -330
  90. package/dist/showmasterpiece/migration/index.d.mts +0 -120
  91. package/dist/showmasterpiece/migration/index.d.ts +0 -120
  92. package/dist/showmasterpiece/migration/index.js +0 -595
  93. package/dist/showmasterpiece/migration/index.js.map +0 -1
  94. package/dist/showmasterpiece/migration/index.mjs +0 -589
  95. package/dist/showmasterpiece/migration/index.mjs.map +0 -1
  96. package/dist/showmasterpiece/scripts/index.d.mts +0 -28
  97. package/dist/showmasterpiece/scripts/index.d.ts +0 -28
  98. package/dist/showmasterpiece/scripts/index.js +0 -327
  99. package/dist/showmasterpiece/scripts/index.js.map +0 -1
  100. package/dist/showmasterpiece/scripts/index.mjs +0 -325
  101. package/dist/showmasterpiece/scripts/index.mjs.map +0 -1
@@ -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"]}