@bittersmilezzz/dsh-asr-voice 0.3.1
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/LICENSE +21 -0
- package/README.md +325 -0
- package/cordis.patch.yml +32 -0
- package/lib/asr-models.js +83 -0
- package/lib/asr-models.js.map +1 -0
- package/lib/client.js +6706 -0
- package/lib/client.js.map +1 -0
- package/lib/http.js +113 -0
- package/lib/http.js.map +1 -0
- package/lib/index.js +193 -0
- package/lib/index.js.map +1 -0
- package/lib/key-ref.js +25 -0
- package/lib/key-ref.js.map +1 -0
- package/lib/optimize.js +168 -0
- package/lib/optimize.js.map +1 -0
- package/lib/presets.js +98 -0
- package/lib/presets.js.map +1 -0
- package/lib/realtime-dashscope.js +211 -0
- package/lib/realtime-dashscope.js.map +1 -0
- package/lib/realtime-host.js +413 -0
- package/lib/realtime-host.js.map +1 -0
- package/lib/realtime-provider.js +143 -0
- package/lib/realtime-provider.js.map +1 -0
- package/lib/realtime-tts.js +179 -0
- package/lib/realtime-tts.js.map +1 -0
- package/lib/settings.js +155 -0
- package/lib/settings.js.map +1 -0
- package/lib/stats.js +30 -0
- package/lib/stats.js.map +1 -0
- package/lib/transcribe.js +274 -0
- package/lib/transcribe.js.map +1 -0
- package/lib/types/asr-models.d.ts +30 -0
- package/lib/types/asr-models.d.ts.map +1 -0
- package/lib/types/http.d.ts +41 -0
- package/lib/types/http.d.ts.map +1 -0
- package/lib/types/index.d.ts +75 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/key-ref.d.ts +9 -0
- package/lib/types/key-ref.d.ts.map +1 -0
- package/lib/types/optimize.d.ts +41 -0
- package/lib/types/optimize.d.ts.map +1 -0
- package/lib/types/presets.d.ts +64 -0
- package/lib/types/presets.d.ts.map +1 -0
- package/lib/types/realtime-dashscope.d.ts +41 -0
- package/lib/types/realtime-dashscope.d.ts.map +1 -0
- package/lib/types/realtime-host.d.ts +95 -0
- package/lib/types/realtime-host.d.ts.map +1 -0
- package/lib/types/realtime-provider.d.ts +76 -0
- package/lib/types/realtime-provider.d.ts.map +1 -0
- package/lib/types/realtime-tts.d.ts +42 -0
- package/lib/types/realtime-tts.d.ts.map +1 -0
- package/lib/types/settings.d.ts +143 -0
- package/lib/types/settings.d.ts.map +1 -0
- package/lib/types/stats.d.ts +29 -0
- package/lib/types/stats.d.ts.map +1 -0
- package/lib/types/transcribe.d.ts +46 -0
- package/lib/types/transcribe.d.ts.map +1 -0
- package/lib/types/tsconfig.client.tsbuildinfo +1 -0
- package/package.json +146 -0
package/lib/http.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** body 读取阶段的错误:自带语义化 HTTP 状态码(400 非法 JSON / 408 读取超时 / 413 超限),
|
|
2
|
+
* 由路由 catch 直接映射。早先这些错误只带 message、路由一律回 502,把「客户端发了 30MB
|
|
3
|
+
* 音频」记成「上游故障」——排查时被引向服务商,实际是本地就能判定并立刻纠正的输入问题。
|
|
4
|
+
* 实测:`req.destroy(err)` 抛出的就是这个对象本身(instanceof 存活),超限在 for-await
|
|
5
|
+
* 里直接 throw 也不会毁掉 socket,413 能真正写到客户端。 */
|
|
6
|
+
export class HttpBodyError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(status, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'HttpBodyError';
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** 从 catch 到的错误取应回的状态码:{@link HttpBodyError} 用自带 status,其余回退 fallback
|
|
15
|
+
* (路由传 502 = 真正的上游/服务端故障)。 */
|
|
16
|
+
export function statusOfBodyError(error, fallback) {
|
|
17
|
+
return error instanceof HttpBodyError ? error.status : fallback;
|
|
18
|
+
}
|
|
19
|
+
/** 读取请求原始 body(Buffer),超限报错;读取停滞超过 timeoutMs 则销毁连接。 */
|
|
20
|
+
export async function readRawBody(req, maxBytes, timeoutMs = 60_000) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
let size = 0;
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
// 慢速/停滞上传不该长期占用 handler 与 socket;destroy 让 for-await 抛错走错误分支。
|
|
25
|
+
// 注:destroy 会连 socket 一起收掉,408 实际到不了客户端(连接已断)——状态码只用于
|
|
26
|
+
// 日志/测试口径,与 413 不同(后者 socket 仍活,响应真的送达)。
|
|
27
|
+
req.destroy(new HttpBodyError(408, `request body read timed out after ${timeoutMs}ms`));
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
try {
|
|
30
|
+
for await (const chunk of req) {
|
|
31
|
+
const buf = chunk;
|
|
32
|
+
size += buf.length;
|
|
33
|
+
if (size > maxBytes)
|
|
34
|
+
throw new HttpBodyError(413, `request body exceeds ${maxBytes} bytes`);
|
|
35
|
+
parts.push(buf);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
return Buffer.concat(parts);
|
|
42
|
+
}
|
|
43
|
+
/** 读取请求 body 并解析为 JSON(超限报错)。 */
|
|
44
|
+
export async function readJsonBody(req, maxBytes = 256 * 1024) {
|
|
45
|
+
const raw = await readRawBody(req, maxBytes);
|
|
46
|
+
if (raw.length === 0)
|
|
47
|
+
return {};
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw.toString('utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new HttpBodyError(400, 'invalid JSON body');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** 写 JSON 响应。 */
|
|
56
|
+
export function sendJson(res, status, payload) {
|
|
57
|
+
const body = JSON.stringify(payload);
|
|
58
|
+
res.writeHead(status, {
|
|
59
|
+
'content-type': 'application/json; charset=utf-8',
|
|
60
|
+
'cache-control': 'no-store',
|
|
61
|
+
'content-length': Buffer.byteLength(body),
|
|
62
|
+
});
|
|
63
|
+
res.end(body);
|
|
64
|
+
}
|
|
65
|
+
/** 信任围栏:只接受本机回环请求,防止任意网页 CSRF 借宿主代理调用云端
|
|
66
|
+
* (消耗用户的 API key / 额度)。要点(与 dsh-retry-settings 的 index.ts、
|
|
67
|
+
* dsh-email 的 web.ts 围栏同款):
|
|
68
|
+
* - sec-fetch-site === 'cross-site' 一票拒绝(浏览器注入、页面无法伪造);
|
|
69
|
+
* - Host 严格全等判定回环(127. 宽前缀会被 127.0.0.1.evil.com 之类 DNS rebinding 绕过);
|
|
70
|
+
* - 带 Origin 的请求要求同源且 Host 本身是回环——仅凭 originName === hostName 时,
|
|
71
|
+
* 解析到 127.0.0.1 的攻击者域名(rebinding 惯用手法)即构成 Host/Origin 相等的
|
|
72
|
+
* "同源"表象,Host 非回环一律不可信;
|
|
73
|
+
* - Origin/Host 解析失败(如字面量 Origin: null)一律不可信,不抛异常冒泡路由。
|
|
74
|
+
*/
|
|
75
|
+
export function isTrusted(req) {
|
|
76
|
+
const site = req.headers['sec-fetch-site'];
|
|
77
|
+
if (typeof site === 'string' && site === 'cross-site')
|
|
78
|
+
return false;
|
|
79
|
+
const stripBrackets = (h) => (h.startsWith('[') && h.endsWith(']') ? h.slice(1, -1) : h);
|
|
80
|
+
const loopbackOf = (h) => h === 'localhost' || h === '::1' || /^127\.\d+\.\d+\.\d+$/.test(h);
|
|
81
|
+
let hostName = '';
|
|
82
|
+
try {
|
|
83
|
+
hostName = stripBrackets(new URL(`http://${String(req.headers.host ?? 'invalid.invalid')}`).hostname.toLowerCase());
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const hostLoopback = loopbackOf(hostName);
|
|
89
|
+
const originHeader = req.headers.origin;
|
|
90
|
+
if (originHeader === undefined)
|
|
91
|
+
return hostLoopback; // 无 Origin(curl/页面导航):只信回环 Host
|
|
92
|
+
let originName = '';
|
|
93
|
+
try {
|
|
94
|
+
originName = stripBrackets(new URL(String(originHeader)).hostname.toLowerCase());
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false; // Origin: null / 畸形 → 不可信
|
|
98
|
+
}
|
|
99
|
+
// 有 Origin:必须同源且 Host 本是回环。仅凭 originName === hostName 时,攻击者可注册
|
|
100
|
+
// 一个解析到 127.0.0.1 的域名(如 127.0.0.1.evil.com,DNS rebinding 惯用手法),本机浏览器
|
|
101
|
+
// 访问它即构成 Host/Origin 相等的"同源"请求——与"无 Origin 但 Host 非回环 → 拒绝"的
|
|
102
|
+
// 既有口径一致:Host 非回环一律不可信。
|
|
103
|
+
return hostLoopback && originName === hostName;
|
|
104
|
+
}
|
|
105
|
+
/** 路由守卫:信任围栏 + method 白名单。通过返回 null;不通过返回已写好的 403/405 响应值(handler 直接 return 它)。 */
|
|
106
|
+
export function guardRoute(req, methods = ['POST']) {
|
|
107
|
+
if (!isTrusted(req))
|
|
108
|
+
return { ok: false, status: 403, payload: { ok: false, reason: 'forbidden: host/origin not trusted' } };
|
|
109
|
+
if (!methods.includes(req.method ?? ''))
|
|
110
|
+
return { ok: false, status: 405, payload: { ok: false, reason: 'method not allowed' } };
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=http.js.map
|
package/lib/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ASR_VOICE_SETTINGS_NAMESPACE, AsrVoiceSettingsSchema } from "./settings.js";
|
|
2
|
+
import { keyRefFor } from "./key-ref.js";
|
|
3
|
+
import { registerTranscribeRoute, resolveApiKey } from "./transcribe.js";
|
|
4
|
+
import { registerOptimizeRoute, registerModelsRoute } from "./optimize.js";
|
|
5
|
+
import { registerAsrModelsRoute } from "./asr-models.js";
|
|
6
|
+
import { createAsrStats, registerStatsRoute } from "./stats.js";
|
|
7
|
+
import { RealtimeHost } from "./realtime-host.js";
|
|
8
|
+
import { createFakeRealtimeProvider } from "./realtime-provider.js";
|
|
9
|
+
import { createDashscopeRealtimeProvider } from "./realtime-dashscope.js";
|
|
10
|
+
import { realtimePresetById, REALTIME_PRESETS } from "./presets.js";
|
|
11
|
+
import { registerTtsRoute } from "./realtime-tts.js";
|
|
12
|
+
export const name = 'dsh-asr-voice';
|
|
13
|
+
/** 所需 Cordis 服务(服务名,非 entry id)。 */
|
|
14
|
+
// settings / agentDefaultModel 不作为硬依赖:settings 用 scoped inject(缺失时仅云端
|
|
15
|
+
// ASR 无 key 可用、不影响挂载),agentDefaultModel 在优化路由内 ctx.get 可选读取。
|
|
16
|
+
export const inject = ['webServer', 'llm'];
|
|
17
|
+
/**
|
|
18
|
+
* 供应商行归一化:settings 深层结构(多供应商/旧单配置)里同一组字段的取值
|
|
19
|
+
* 规则一致(缺省 preset=openai、mode=auto、其余空串),所有读取路径共用
|
|
20
|
+
* 这一个视图,避免四处维护同一份兜底规则。
|
|
21
|
+
* @param row 供应商原始行(name/id 均可缺省:name 用于显示,id 用于引用派生)。
|
|
22
|
+
* @param idFallback 行缺 id 时的兜底 id。
|
|
23
|
+
* @param modeOverride 显式 mode(undefined = 取行内 mode,缺省 'auto')。
|
|
24
|
+
*/
|
|
25
|
+
// 内部导出(非插件公共 API):纯函数 + 可注入依赖,node 单测直连
|
|
26
|
+
// (test/index-logic.test.mjs),不为此搭 Cordis mock 基座。
|
|
27
|
+
export function providerView(row, idFallback, modeOverride) {
|
|
28
|
+
return {
|
|
29
|
+
id: row.id || idFallback,
|
|
30
|
+
preset: row.preset ?? 'openai',
|
|
31
|
+
name: row.name ?? '',
|
|
32
|
+
baseUrl: row.baseUrl ?? '',
|
|
33
|
+
apiKey: row.apiKey ?? '',
|
|
34
|
+
model: row.model ?? '',
|
|
35
|
+
mode: modeOverride ?? row.mode ?? 'auto',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** 从 settings 解析当前生效的云端 ASR 供应商(多供应商 active/首个,或旧单配置)。
|
|
39
|
+
* 内部导出:单测直连。 */
|
|
40
|
+
export function resolveCloudProvider(v) {
|
|
41
|
+
if (!v)
|
|
42
|
+
return undefined;
|
|
43
|
+
const cloud = v.asr.cloud;
|
|
44
|
+
if (Array.isArray(cloud.providers) && cloud.providers.length > 0) {
|
|
45
|
+
const active = cloud.providers.find((p) => p.id === cloud.active) ?? cloud.providers[0];
|
|
46
|
+
return providerView(active, 'provider');
|
|
47
|
+
}
|
|
48
|
+
// 旧单配置(v0.1 遗留)
|
|
49
|
+
return providerView({ ...cloud, id: 'legacy' }, 'legacy');
|
|
50
|
+
}
|
|
51
|
+
/** 读取全部已配置供应商(多供应商列表;旧单配置合成一个 'legacy')。
|
|
52
|
+
* 内部导出:单测直连。 */
|
|
53
|
+
export function listProviders(v) {
|
|
54
|
+
if (!v)
|
|
55
|
+
return [];
|
|
56
|
+
const cloud = v.asr.cloud;
|
|
57
|
+
if (Array.isArray(cloud.providers) && cloud.providers.length > 0) {
|
|
58
|
+
return cloud.providers.map((p) => providerView(p, 'provider'));
|
|
59
|
+
}
|
|
60
|
+
if (cloud.baseUrl) {
|
|
61
|
+
return [providerView({ ...cloud, id: 'legacy' }, 'legacy')];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
/** 一次性迁移:把 settings 里遗留的明文 API key 搬进 DSH credentials,全部搬成功后抹掉明文。
|
|
66
|
+
* 任一条搬不动(凭据服务缺席、该引用被只读来源拒绝)就整批原样留着——抹掉一把无处可寻的
|
|
67
|
+
* key 比留一份本机明文更糟。{@link resolveApiKey} 始终先读 settings,所以未迁移状态下功能
|
|
68
|
+
* 不降级;迁移成功后 settings 里的 key 恒为空,密钥只剩 credentials 一个来源。
|
|
69
|
+
* 内部导出(非插件公共 API):scope/credentials/log 全部可注入,node 单测直连。
|
|
70
|
+
*/
|
|
71
|
+
export async function migrateLegacyKeys(scope, credentials, log) {
|
|
72
|
+
if (credentials === undefined)
|
|
73
|
+
return;
|
|
74
|
+
// scope.get() 交回来的是深度冻结的解析快照,改之前必须先脱冻。
|
|
75
|
+
const cloud = structuredClone(scope.get().asr.cloud);
|
|
76
|
+
const pending = [];
|
|
77
|
+
for (const row of cloud.providers) {
|
|
78
|
+
const key = (row.apiKey ?? '').trim();
|
|
79
|
+
if (key === '')
|
|
80
|
+
continue;
|
|
81
|
+
pending.push({ ref: keyRefFor(row), key });
|
|
82
|
+
row.apiKey = '';
|
|
83
|
+
}
|
|
84
|
+
const legacyKey = (cloud.apiKey ?? '').trim();
|
|
85
|
+
if (legacyKey !== '') {
|
|
86
|
+
pending.push({ ref: keyRefFor({ preset: cloud.preset, name: '', id: 'legacy' }), key: legacyKey });
|
|
87
|
+
}
|
|
88
|
+
if (pending.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
for (const item of pending) {
|
|
91
|
+
try {
|
|
92
|
+
await credentials.set(item.ref, item.key);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
96
|
+
log.warn(`credentials.set(${item.ref}) refused: ${reason}; legacy keys left in place`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const patch = { providers: cloud.providers };
|
|
101
|
+
if (legacyKey !== '')
|
|
102
|
+
patch.apiKey = '';
|
|
103
|
+
await scope.update({ asr: { cloud: patch } });
|
|
104
|
+
log.info(`moved ${pending.length} API key(s) from plugin settings into DSH credentials`);
|
|
105
|
+
}
|
|
106
|
+
/** 实时 provider 工厂(I5):按 `realtime.provider` 设置分派。
|
|
107
|
+
* '' / 'builtin' → 内置假 provider(I3/I4 开发态,不花配额);
|
|
108
|
+
* 预置 id(如 'dashscope-realtime')→ 真云端 provider,凭据复用 DSH credentials
|
|
109
|
+
* (keyPreset 指回 CLOUD_PRESETS 预置,`keyRefFor` 派生成 `<PRESET>_API_KEY`,
|
|
110
|
+
* 配过同名 LLM 的用户天然命中同一把 key)。无 key 时 connect 抛错,让会话路由
|
|
111
|
+
* 502 带原因,客户端可见「provider-unreachable」而不是静默降级。
|
|
112
|
+
* 未知 id 同样抛错(不回落假 provider)——见下方注释。
|
|
113
|
+
*/
|
|
114
|
+
function createRealtimeProvider(ctx, getSettings) {
|
|
115
|
+
const pid = getSettings()?.realtime.provider ?? '';
|
|
116
|
+
if (pid === '' || pid === 'builtin')
|
|
117
|
+
return createFakeRealtimeProvider();
|
|
118
|
+
const preset = realtimePresetById(pid);
|
|
119
|
+
if (preset === undefined) {
|
|
120
|
+
// 未知 id **不静默回落到内置模拟**:那会让用户以为在用云端,实际拿到「模拟转写·第N段」
|
|
121
|
+
// 并被当成真实转写提交。抛错 → 会话路由 502 带原因 → 客户端提示云端通道不可用。
|
|
122
|
+
// 只在手改配置/预置被改名时才会走到(设置页的下拉只列 REALTIME_PRESETS)。
|
|
123
|
+
return {
|
|
124
|
+
connect: async () => {
|
|
125
|
+
throw new Error(`unknown realtime provider "${pid}" — expected '' / 'builtin' or one of: ${REALTIME_PRESETS.map((p) => p.id).join(', ')}`);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
connect: async () => {
|
|
131
|
+
const apiKey = await resolveApiKey(ctx, {
|
|
132
|
+
id: preset.id,
|
|
133
|
+
preset: preset.keyPreset,
|
|
134
|
+
name: preset.label,
|
|
135
|
+
baseUrl: '',
|
|
136
|
+
apiKey: '',
|
|
137
|
+
model: preset.defaultModel,
|
|
138
|
+
mode: 'chat',
|
|
139
|
+
});
|
|
140
|
+
if (apiKey === '') {
|
|
141
|
+
throw new Error(`realtime provider ${pid}: no API key — set the credential ${keyRefFor({ preset: preset.keyPreset, name: preset.label, id: preset.id })} in DSH (a same-named LLM key is reused automatically)`);
|
|
142
|
+
}
|
|
143
|
+
return createDashscopeRealtimeProvider({ apiKey, model: preset.defaultModel, wssUrl: preset.wssUrl }).connect();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function apply(ctx) {
|
|
148
|
+
// 插件配置 namespace:设置统一存 host settings 服务(namespace `asr-voice`)。
|
|
149
|
+
let settingsScope;
|
|
150
|
+
ctx.inject(['settings'], (sctx) => {
|
|
151
|
+
const scope = sctx.settings.register(ASR_VOICE_SETTINGS_NAMESPACE, AsrVoiceSettingsSchema);
|
|
152
|
+
settingsScope = scope;
|
|
153
|
+
// 遗留明文 key → credentials:启动时跑一次,迁移完成后每次都是空转早退。
|
|
154
|
+
const logger = sctx.logger('asr-voice');
|
|
155
|
+
sctx.effect(() => {
|
|
156
|
+
const migrating = migrateLegacyKeys(scope, sctx.get('credentials'), logger)
|
|
157
|
+
.catch((error) => { logger.warn(`legacy key migration stopped: ${error instanceof Error ? error.message : String(error)}`); });
|
|
158
|
+
return async () => { await migrating; };
|
|
159
|
+
}, 'asr-voice: migrate legacy api keys');
|
|
160
|
+
});
|
|
161
|
+
const getCloudConfig = () => {
|
|
162
|
+
const cfg = resolveCloudProvider(settingsScope?.get());
|
|
163
|
+
return cfg ?? { id: '', preset: 'openai', name: '', baseUrl: '', apiKey: '', model: '', mode: 'auto' };
|
|
164
|
+
};
|
|
165
|
+
const getProviders = () => listProviders(settingsScope?.get());
|
|
166
|
+
const stats = createAsrStats();
|
|
167
|
+
// 路由随 fiber 生命周期注册/回收。
|
|
168
|
+
ctx.effect(() => registerTranscribeRoute((def) => ctx.webServer.register(def), getCloudConfig, ctx, (text, providerId) => stats.record(text, providerId)), 'asr-voice: transcribe route');
|
|
169
|
+
ctx.effect(() => registerOptimizeRoute((def) => ctx.webServer.register(def), ctx), 'asr-voice: optimize route');
|
|
170
|
+
ctx.effect(() => registerModelsRoute((def) => ctx.webServer.register(def), ctx), 'asr-voice: models route');
|
|
171
|
+
ctx.effect(() => registerAsrModelsRoute((def) => ctx.webServer.register(def), getProviders, ctx), 'asr-voice: asr-models route');
|
|
172
|
+
ctx.effect(() => registerStatsRoute((def) => ctx.webServer.register(def), () => stats.snapshot()), 'asr-voice: stats route');
|
|
173
|
+
// 云端 TTS(I6):文本 → 私有路由 → qwen3-tts-flash-realtime,浏览器不持 key。
|
|
174
|
+
// 凭据固定复用 DASHSCOPE_API_KEY(getTtsConfig 只喂 preset=dashscope 的空壳配置,
|
|
175
|
+
// resolveApiKey 按 keyRefFor 派生引用名,配过同名 LLM 的用户天然命中)。
|
|
176
|
+
ctx.effect(() => registerTtsRoute((def) => ctx.webServer.register(def), () => ({ id: 'tts', preset: 'dashscope', name: '', baseUrl: '', apiKey: '', model: '', mode: 'chat' }), ctx), 'asr-voice: tts route');
|
|
177
|
+
// 实时转写通道(I3):会话注册表 + SSE 下行 + RealtimeProvider 接缝。
|
|
178
|
+
// I3/I4 阶段 host 用假 provider 驱动整条管道;I5 按 settings `realtime.provider`
|
|
179
|
+
// 分派到真云端(qwen3-asr-flash-realtime)或内置模拟——接缝与路由一行不改。
|
|
180
|
+
ctx.effect(() => {
|
|
181
|
+
const host = new RealtimeHost({
|
|
182
|
+
createProvider: () => createRealtimeProvider(ctx, () => settingsScope?.get()).connect(),
|
|
183
|
+
});
|
|
184
|
+
const disposeRoutes = host.registerRoutes((def) => ctx.webServer.register(def));
|
|
185
|
+
return () => {
|
|
186
|
+
disposeRoutes();
|
|
187
|
+
// 会话清理归 fiber(在注销路由之外):卸载/热重载时逐个拆掉活动会话——
|
|
188
|
+
// SSE 心跳、idle timer、provider WS 全部随 closeSession 释放,不泄漏到进程结束。
|
|
189
|
+
host.dispose();
|
|
190
|
+
};
|
|
191
|
+
}, 'asr-voice: realtime routes');
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/key-ref.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** dsh-asr-voice — API key 的凭据引用名(host / client 共用的唯一真相)。
|
|
2
|
+
* key 既不存 settings 也不进浏览器:统一落在 DSH credentials 服务里,按引用名读取。
|
|
3
|
+
* 预置供应商刻意沿用官方 LLM provider 的命名(`<PROVIDER>_API_KEY`,派生方式见
|
|
4
|
+
* packages/client/ui-settings-models/src/client/store.ts 的 deriveKeyRef),因此用户
|
|
5
|
+
* 在 DSH 里配过 OpenAI / Groq / MiMo / 百炼的 LLM,ASR 直接复用同一把 key,一次都不用填。
|
|
6
|
+
* 自定义供应商带一个可读 name,派生成 `ASR_VOICE_<NAME>_API_KEY`,不与其它插件撞名。
|
|
7
|
+
* 引用名受网关校验:必须是 POSIX 环境变量形状 `^[A-Za-z_][A-Za-z0-9_]*$`
|
|
8
|
+
* (packages/host/apiproxy/src/api/credentials.schema.ts),故非字母数字一律折叠成 `_`。
|
|
9
|
+
*/
|
|
10
|
+
import { CLOUD_PRESETS } from "./presets.js";
|
|
11
|
+
/** 可复用官方 LLM 凭据的预置 id 集合(其余一律按自定义处理)。 */
|
|
12
|
+
const PRESET_IDS = new Set(CLOUD_PRESETS.map((p) => p.id));
|
|
13
|
+
/** 任意标签 → 合法引用名片段(无可用字符时返回空串,由调用方决定退路)。 */
|
|
14
|
+
function slug(raw) {
|
|
15
|
+
return raw.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
16
|
+
}
|
|
17
|
+
/** 求某供应商的 API key 引用名。 @param p - 供应商身份(预置 id、显示名、行 id)。 @returns credentials 服务里的引用名。 */
|
|
18
|
+
export function keyRefFor(p) {
|
|
19
|
+
if (PRESET_IDS.has(p.preset))
|
|
20
|
+
return `${slug(p.preset)}_API_KEY`;
|
|
21
|
+
const fromName = slug(p.name);
|
|
22
|
+
const label = fromName !== '' ? fromName : slug(p.id);
|
|
23
|
+
return `ASR_VOICE_${label === '' ? 'CUSTOM' : label}_API_KEY`;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=key-ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-ref.js","sourceRoot":"","sources":["../src/key-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,yCAAyC;AACzC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAE1D,2CAA2C;AAC3C,SAAS,IAAI,CAAC,GAAW;IACvB,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;AAC9E,CAAC;AASD,0FAA0F;AAC1F,MAAM,UAAU,SAAS,CAAC,CAAe;IACvC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;QAAE,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAA;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7B,MAAM,KAAK,GAAG,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACrD,OAAO,aAAa,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,UAAU,CAAA;AAC/D,CAAC"}
|
package/lib/optimize.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { guardRoute, readJsonBody, sendJson, statusOfBodyError } from "./http.js";
|
|
3
|
+
import { LIST_MODELS_TIMEOUT_MS } from "./asr-models.js";
|
|
4
|
+
/** LLM 优化流超时(ms):模型卡住/过慢时不把宿主流挂死。 */
|
|
5
|
+
const LLM_STREAM_TIMEOUT_MS = 60_000;
|
|
6
|
+
/** 提示词优化 system prompt(中英双语指令,要求保留语义、去掉口语、结构化)。 */
|
|
7
|
+
const OPTIMIZE_SYSTEM = [
|
|
8
|
+
'你是语音输入的提示词优化器。',
|
|
9
|
+
'把用户口语转写的文字整理成清晰、可直接发给 AI 助手的提示词:',
|
|
10
|
+
'- 去掉语气词、重复、口误(如“嗯”“那个”“就是说”)。',
|
|
11
|
+
'- 补齐标点与分段,修正明显口误。',
|
|
12
|
+
'- 保留用户的真实意图与全部关键信息,不添加原文没有的内容。',
|
|
13
|
+
'- 用原文语言输出(中文保持中文,英文保持英文)。',
|
|
14
|
+
'- 只输出整理后的文本,不要解释、不要加引号或前后缀。',
|
|
15
|
+
].join('\n');
|
|
16
|
+
/** 单个 provider 模型枚举的竞速超时:与 asr-models 的抓取超时同源(见该处导出)。 */
|
|
17
|
+
/**
|
|
18
|
+
* 给 promise 套整体超时。
|
|
19
|
+
* ctx.llm.listModels 的接口签名没有 AbortSignal(见 dsh-llm 的 LlmService.listModels,
|
|
20
|
+
* 与 LlmAdapter.listModels 一致),无法真正取消底层调用——竞速是唯一手段:超时后
|
|
21
|
+
* 外层立刻 settle,迟到的底层结果经 settled 标志丢弃(不会 unhandled rejection)。
|
|
22
|
+
* 两个分支都 clearTimeout,timer 不泄漏。
|
|
23
|
+
*/
|
|
24
|
+
function withTimeout(p, ms) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let settled = false;
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
if (settled)
|
|
29
|
+
return;
|
|
30
|
+
settled = true;
|
|
31
|
+
reject(new Error(`listModels timed out after ${ms}ms`));
|
|
32
|
+
}, ms);
|
|
33
|
+
p.then((v) => { if (settled)
|
|
34
|
+
return; settled = true; clearTimeout(timer); resolve(v); }, (e) => { if (settled)
|
|
35
|
+
return; settled = true; clearTimeout(timer); reject(e); });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** 枚举 DSH 已配置模型(ctx.llm.listProviders + listModels)。 枚举失败/不可用/超时的 provider 给空模型列表(不阻断整体)。 */
|
|
39
|
+
export async function enumerateModels(ctx) {
|
|
40
|
+
const providers = ctx.llm.listProviders();
|
|
41
|
+
const results = await Promise.all(providers.map(async (p) => {
|
|
42
|
+
let models = [];
|
|
43
|
+
try {
|
|
44
|
+
const listed = await withTimeout(Promise.resolve(ctx.llm.listModels(p.id)), LIST_MODELS_TIMEOUT_MS);
|
|
45
|
+
models = listed.map((m) => ({ id: m.id, name: m.name }));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// 该 provider 不可枚举(含超时):跳过模型(仍保留 provider 行,便于提示)。
|
|
49
|
+
}
|
|
50
|
+
return { provider: p.id, name: p.name, models };
|
|
51
|
+
}));
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 每次调用分配一个新鲜的调用方 session ID。
|
|
56
|
+
*
|
|
57
|
+
* 部分网关(如 opencode-go 的 Console Go 路由)要求请求带 x-opencode-session
|
|
58
|
+
* 才能路由:GenerateOptions.sessionId 存在时 llm-pi-ai 适配器才会把它注入
|
|
59
|
+
* 会话头,缺失时网关直接 400 MissingSessionID(表现为"模型没有返回文本")。
|
|
60
|
+
* 聊天回路会由 agent-loop 打上真正的 DSH SessionId;插件侧没有会话绑定,
|
|
61
|
+
* 用随机 ID 即可——网关接受任意值(按需自建会话),不带 sessionHeader 的
|
|
62
|
+
* provider(deepseek-official / xiaomi 等)忽略该字段,实测无副作用。
|
|
63
|
+
*/
|
|
64
|
+
function newCallSessionId() {
|
|
65
|
+
return `dsh-asr-voice-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
66
|
+
}
|
|
67
|
+
/** 用 DSH 的 LLM 通道重写文本(target 缺省 = 当前所选模型)。 */
|
|
68
|
+
async function optimizeWithLlm(ctx, text, target) {
|
|
69
|
+
let selection;
|
|
70
|
+
if (target !== undefined && target.provider !== '' && target.model !== '') {
|
|
71
|
+
selection = target;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const agentDefaultModel = ctx.get('agentDefaultModel');
|
|
75
|
+
if (agentDefaultModel === undefined) {
|
|
76
|
+
throw new Error('LLM optimize unavailable: agentDefaultModel service not present');
|
|
77
|
+
}
|
|
78
|
+
const current = agentDefaultModel.currentSelection();
|
|
79
|
+
selection = { provider: current.provider, model: current.model };
|
|
80
|
+
}
|
|
81
|
+
// GenerateOptions.sessionId 是 dsh-brand 名义类型(会话头只随它下发),
|
|
82
|
+
// 插件侧无法 import 该 brand(非直接依赖),调用处做一次断言即可——运行时无校验。
|
|
83
|
+
const options = {
|
|
84
|
+
provider: selection.provider,
|
|
85
|
+
model: selection.model,
|
|
86
|
+
system: OPTIMIZE_SYSTEM,
|
|
87
|
+
sessionId: newCallSessionId(),
|
|
88
|
+
messages: [
|
|
89
|
+
createUserMessage({
|
|
90
|
+
content: [{ type: 'text', text }],
|
|
91
|
+
source: { kind: 'plugin', plugin: 'dsh-asr-voice' },
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
temperature: 0.2,
|
|
95
|
+
// 模型卡住/过慢时不把宿主流挂死(客户端 60s 超时后 host 应停止等待)。
|
|
96
|
+
signal: AbortSignal.timeout(LLM_STREAM_TIMEOUT_MS),
|
|
97
|
+
};
|
|
98
|
+
// 流式文本累积用数组 + join(避免长响应对每 chunk 反复拼接字符串)。
|
|
99
|
+
const parts = [];
|
|
100
|
+
// 上游失败的真实原因(错误码/网关响应体)只在 finish 的 failure 里,取出来透出给用户,
|
|
101
|
+
// 否则一律变成笼统的 "model returned no text"(如 400 MissingSessionID 曾被完全吞掉)。
|
|
102
|
+
let finishFailure;
|
|
103
|
+
for await (const chunk of ctx.llm.stream(options)) {
|
|
104
|
+
if (chunk.type === 'text-delta')
|
|
105
|
+
parts.push(chunk.text);
|
|
106
|
+
if (chunk.type === 'finish' && (chunk.reason.kind === 'error' || chunk.reason.kind === 'aborted')) {
|
|
107
|
+
finishFailure = chunk.reason.failure.message;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const output = parts.join('');
|
|
111
|
+
if (finishFailure !== undefined && finishFailure !== '') {
|
|
112
|
+
const detail = finishFailure.length > 240 ? `${finishFailure.slice(0, 240)}…` : finishFailure;
|
|
113
|
+
throw new Error(`LLM optimize failed: ${detail}`);
|
|
114
|
+
}
|
|
115
|
+
if (output.trim() === '') {
|
|
116
|
+
throw new Error('LLM optimize failed: model returned no text');
|
|
117
|
+
}
|
|
118
|
+
return output.trim();
|
|
119
|
+
}
|
|
120
|
+
/** 注册 /api/asr-voice/optimize 路由。 请求体:{ text, provider?, model? }——provider/model 须为 DSH 已配置模型; 缺省用当前所选 LLM。 */
|
|
121
|
+
export function registerOptimizeRoute(register, ctx) {
|
|
122
|
+
return register({
|
|
123
|
+
kind: 'exact',
|
|
124
|
+
path: '/api/asr-voice/optimize',
|
|
125
|
+
handler: async (req, res) => {
|
|
126
|
+
const denied = guardRoute(req);
|
|
127
|
+
if (denied !== null)
|
|
128
|
+
return sendJson(res, denied.status, denied.payload);
|
|
129
|
+
try {
|
|
130
|
+
const body = (await readJsonBody(req));
|
|
131
|
+
if (typeof body.text !== 'string' || body.text.trim() === '') {
|
|
132
|
+
return sendJson(res, 400, { ok: false, reason: 'missing text' });
|
|
133
|
+
}
|
|
134
|
+
const target = typeof body.provider === 'string' && typeof body.model === 'string'
|
|
135
|
+
? { provider: body.provider, model: body.model }
|
|
136
|
+
: undefined;
|
|
137
|
+
const optimized = await optimizeWithLlm(ctx, body.text, target);
|
|
138
|
+
return sendJson(res, 200, { ok: true, text: optimized });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
142
|
+
// 非法 JSON / 超限 / 超时都是请求侧问题(400/413/408),只有 LLM 通道故障才是 502。
|
|
143
|
+
return sendJson(res, statusOfBodyError(error, 502), { ok: false, reason });
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** 注册 /api/asr-voice/models 路由:枚举 DSH 已配置模型(设置页选择器用)。 */
|
|
149
|
+
export function registerModelsRoute(register, ctx) {
|
|
150
|
+
return register({
|
|
151
|
+
kind: 'exact',
|
|
152
|
+
path: '/api/asr-voice/models',
|
|
153
|
+
handler: async (req, res) => {
|
|
154
|
+
const denied = guardRoute(req, ['GET']);
|
|
155
|
+
if (denied !== null)
|
|
156
|
+
return sendJson(res, denied.status, denied.payload);
|
|
157
|
+
try {
|
|
158
|
+
const providers = await enumerateModels(ctx);
|
|
159
|
+
return sendJson(res, 200, { ok: true, providers });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
163
|
+
return sendJson(res, 502, { ok: false, reason });
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=optimize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimize.js","sourceRoot":"","sources":["../src/optimize.ts"],"names":[],"mappings":"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"}
|
package/lib/presets.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** dsh-asr-voice — 云端 ASR 预置表(host 与 client 共享,双半区各自编译)。
|
|
2
|
+
* 全部走 OpenAI-compatible `/audio/transcriptions` 端点,统一字段:
|
|
3
|
+
* baseUrl + apiKey + model
|
|
4
|
+
* 预置只是「快捷填充」,用户可任意改 baseUrl / model(自定义端点天然兼容
|
|
5
|
+
* 本地/私有部署的 OpenAI-compatible ASR 服务,如 local-ai)。
|
|
6
|
+
* 跨平台说明:云端 ASR 是纯 HTTP,macOS / Windows 行为一致。
|
|
7
|
+
*/
|
|
8
|
+
/** 内置预置:OpenAI / Groq(国际)+ 硅基流动 / 小米 MiMo / 通义 Qwen-ASR(国产)。 */
|
|
9
|
+
export const CLOUD_PRESETS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'openai',
|
|
12
|
+
label: 'OpenAI Whisper',
|
|
13
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
14
|
+
defaultModel: 'whisper-1',
|
|
15
|
+
mode: 'transcriptions',
|
|
16
|
+
hint: 'OpenAI 官方 /audio/transcriptions',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'groq',
|
|
20
|
+
label: 'Groq Whisper',
|
|
21
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
22
|
+
defaultModel: 'whisper-large-v3',
|
|
23
|
+
mode: 'transcriptions',
|
|
24
|
+
hint: 'Groq 高速推理,whisper-large-v3',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'siliconflow',
|
|
28
|
+
label: '硅基流动 SiliconFlow',
|
|
29
|
+
baseUrl: 'https://api.siliconflow.cn/v1',
|
|
30
|
+
defaultModel: 'FunAudioLLM/SenseVoiceSmall',
|
|
31
|
+
mode: 'transcriptions',
|
|
32
|
+
hint: '国产,SenseVoice 系语音识别',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'mimo',
|
|
36
|
+
label: '小米 MiMo',
|
|
37
|
+
baseUrl: 'https://api.xiaomimimo.com/v1',
|
|
38
|
+
defaultModel: 'mimo-v2.5-asr',
|
|
39
|
+
mode: 'chat',
|
|
40
|
+
hint: '小米 MiMo-V2.5-ASR(OpenAI 兼容 chat input_audio;key 复用 DSH 凭据 MIMO_API_KEY)',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'dashscope',
|
|
44
|
+
label: '通义/阿里云百炼 Qwen-ASR',
|
|
45
|
+
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
46
|
+
defaultModel: 'qwen3-asr-flash',
|
|
47
|
+
mode: 'chat',
|
|
48
|
+
hint: '国产,Qwen-ASR(compatible-mode OpenAI 兼容 chat input_audio)',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
/** 按 id 取预置(找不到返回 undefined)。 */
|
|
52
|
+
export function presetById(id) {
|
|
53
|
+
return CLOUD_PRESETS.find((p) => p.id === id);
|
|
54
|
+
}
|
|
55
|
+
/** 内置实时预置:阿里云百炼 Qwen-ASR Realtime(服务端 VAD 断句)。 */
|
|
56
|
+
export const REALTIME_PRESETS = [
|
|
57
|
+
{
|
|
58
|
+
id: 'builtin',
|
|
59
|
+
label: '内置模拟(开发)',
|
|
60
|
+
wssUrl: '',
|
|
61
|
+
defaultModel: '',
|
|
62
|
+
keyPreset: 'openai',
|
|
63
|
+
hint: 'I3/I4 开发态:host 用能量 VAD 假 provider 驱动整条管道,不花配额',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'dashscope-realtime',
|
|
67
|
+
label: '阿里云百炼 Qwen-ASR Realtime',
|
|
68
|
+
wssUrl: 'wss://dashscope.aliyuncs.com/api-ws/v1/realtime',
|
|
69
|
+
defaultModel: 'qwen3-asr-flash-realtime',
|
|
70
|
+
keyPreset: 'dashscope',
|
|
71
|
+
hint: '服务端 VAD 判回合;key 复用 DSH 凭据 DASHSCOPE_API_KEY(约 ¥1.19/音频小时)',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
/** 按 id 取实时预置(找不到返回 undefined)。 */
|
|
75
|
+
export function realtimePresetById(id) {
|
|
76
|
+
return REALTIME_PRESETS.find((p) => p.id === id);
|
|
77
|
+
}
|
|
78
|
+
/** 预置默认 id。 */
|
|
79
|
+
export const DEFAULT_PRESET_ID = 'openai';
|
|
80
|
+
/** 云端 ASR 端点(拼在 baseUrl 之后,whisper 式通道用)。 */
|
|
81
|
+
export const TRANSCRIBE_PATH = '/audio/transcriptions';
|
|
82
|
+
/** chat-completions 通道端点(拼在 baseUrl 之后)。 */
|
|
83
|
+
export const CHAT_COMPLETIONS_PATH = '/chat/completions';
|
|
84
|
+
/** 上传/代理的音频大小上限(字节),防滥用。 */
|
|
85
|
+
export const MAX_AUDIO_BYTES = 25 * 1024 * 1024;
|
|
86
|
+
/** 模型名含这些词 → 判定为「音频/ASR 大模型」走 chat input_audio 通道。 */
|
|
87
|
+
const CHAT_MODE_MODEL_RE = /(^|[/_.-])(asr|audio|omni)([/_.-]|$)|sensevoice/i;
|
|
88
|
+
/** 按模型名自动判定调用通道:chat 音频大模型 vs whisper 式 /audio/transcriptions。 */
|
|
89
|
+
export function autoModeForModel(model) {
|
|
90
|
+
return CHAT_MODE_MODEL_RE.test(model) ? 'chat' : 'transcriptions';
|
|
91
|
+
}
|
|
92
|
+
/** 解析最终调用通道:显式模式优先,auto 按模型名判定。 */
|
|
93
|
+
export function resolveAsrMode(mode, model) {
|
|
94
|
+
if (mode === 'chat' || mode === 'transcriptions')
|
|
95
|
+
return mode;
|
|
96
|
+
return autoModeForModel(model);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=presets.js.map
|