gewe-openclaw 2026.2.2 → 2026.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -17
- package/package.json +1 -1
- package/src/config-schema.ts +1 -0
- package/src/delivery.ts +136 -3
- package/src/inbound.ts +112 -0
- package/src/silk.ts +13 -3
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -32,21 +32,31 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
32
32
|
|
|
33
33
|
> 安装或启用插件后需要重启 Gateway。
|
|
34
34
|
|
|
35
|
+
## 配置方式(二选一)
|
|
36
|
+
|
|
37
|
+
安装完成后可任选一种方式完成配置:
|
|
38
|
+
|
|
39
|
+
### 方式 A:Onboarding 向导
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
openclaw onboard
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
在通道列表中选择 **GeWe**,按提示填写 `token`、`appId`、`webhook` 与 `mediaPublicUrl` 等信息。
|
|
46
|
+
|
|
47
|
+
### 方式 B:直接编辑配置文件
|
|
48
|
+
|
|
49
|
+
直接编辑 `~/.openclaw/openclaw.json` 的 `channels.gewe-openclaw` 段落(见下方示例)。
|
|
50
|
+
|
|
35
51
|
## 配置
|
|
36
52
|
|
|
37
|
-
插件配置放在 `~/.openclaw/openclaw.json` 的 `channels.gewe-openclaw
|
|
53
|
+
插件配置放在 `~/.openclaw/openclaw.json` 的 `channels.gewe-openclaw`,并确保通道开启(示例仅保留必填/常用字段):
|
|
38
54
|
|
|
39
55
|
```json5
|
|
40
56
|
{
|
|
41
|
-
"plugins": {
|
|
42
|
-
"entries": {
|
|
43
|
-
"gewe-openclaw": { "enabled": true }
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
57
|
"channels": {
|
|
47
58
|
"gewe-openclaw": {
|
|
48
59
|
"enabled": true,
|
|
49
|
-
"apiBaseUrl": "https://www.geweapi.com",
|
|
50
60
|
"token": "<gewe-token>",
|
|
51
61
|
"appId": "<gewe-app-id>",
|
|
52
62
|
"webhookHost": "0.0.0.0",
|
|
@@ -56,18 +66,13 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
56
66
|
"mediaPort": 4400,
|
|
57
67
|
"mediaPath": "/gewe-media",
|
|
58
68
|
"mediaPublicUrl": "https://your-public-domain/gewe-media",
|
|
59
|
-
"allowFrom": ["wxid_xxx"]
|
|
60
|
-
"silkAutoDownload": true,
|
|
61
|
-
"silkVersion": "latest",
|
|
62
|
-
"silkBaseUrl": "https://github.com/Wangnov/rust-silk/releases/download",
|
|
63
|
-
"silkInstallDir": "~/.openclaw/tools/rust-silk",
|
|
64
|
-
"silkAllowUnverified": false
|
|
69
|
+
"allowFrom": ["wxid_xxx"]
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
```
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
完整参数说明:
|
|
71
76
|
- `webhookHost/webhookPort/webhookPath`:GeWe 回调入口(需公网可达,常配合 FRP)。
|
|
72
77
|
- `mediaPath`:本地媒体服务的路由前缀(默认 `/gewe-media`)。
|
|
73
78
|
- `mediaPublicUrl`:公网访问地址的“基础前缀”,会自动拼接媒体 ID。通常应与 `mediaPath` 对齐,例如 `mediaPath="/gewe-media"` 时,`mediaPublicUrl` 也应包含 `/gewe-media`。
|
|
@@ -79,18 +84,28 @@ openclaw plugins install ./gewe-openclaw.tgz
|
|
|
79
84
|
- `silkInstallDir`:自定义安装目录(默认 `~/.openclaw/tools/rust-silk/<version>`)。
|
|
80
85
|
- `silkAllowUnverified`:校验文件缺失时是否允许继续(默认 `false`)。
|
|
81
86
|
- `silkSha256`:手动指定下载包 SHA256(用于私有源或校验文件缺失场景)。
|
|
87
|
+
- `apiBaseUrl`:GeWe API 地址(默认 `https://www.geweapi.com`)。
|
|
88
|
+
- `voiceFfmpegPath`/`videoFfmpegPath`/`videoFfprobePath`:自定义 ffmpeg/ffprobe 路径。
|
|
89
|
+
- `voiceSilkPath`/`voiceSilkArgs`:自定义 silk 编码器路径和参数(不使用自动下载时)。
|
|
90
|
+
- `voiceSilkPipe`:是否启用 ffmpeg+rust-silk 的 stdin/stdout 管道(默认关闭;失败会回退到临时文件)。
|
|
91
|
+
- 低频/非高并发且磁盘压力不高时,推荐临时文件方案(更稳定/更快)。
|
|
92
|
+
- 高频/多并发或磁盘压力大时,推荐 pipe 方案(减少磁盘 IO)。
|
|
93
|
+
- `voiceDecodePath`/`voiceDecodeArgs`/`voiceDecodeOutput`:自定义 silk 解码器(入站语音转写用)。
|
|
94
|
+
- `mediaMaxMb`:上传媒体大小上限(默认 20MB)。
|
|
95
|
+
- `downloadMinDelayMs`/`downloadMaxDelayMs`:入站媒体下载节流。
|
|
82
96
|
|
|
83
97
|
> 配置变更后需重启 Gateway。
|
|
84
98
|
|
|
85
|
-
##
|
|
99
|
+
## 高级用法:让未安装插件也出现在 onboarding 列表
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
默认情况下,**只有已安装的插件**会出现在 onboarding 列表中。
|
|
102
|
+
如果你希望“未安装时也能在列表中展示”,需要配置本地 catalog:
|
|
88
103
|
|
|
89
104
|
```
|
|
90
105
|
~/.openclaw/plugins/catalog.json
|
|
91
106
|
```
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
示例(添加一次即可):
|
|
94
109
|
|
|
95
110
|
```json
|
|
96
111
|
{
|
package/package.json
CHANGED
package/src/config-schema.ts
CHANGED
|
@@ -44,6 +44,7 @@ export const GeweAccountSchemaBase = z
|
|
|
44
44
|
voiceFfmpegPath: z.string().optional(),
|
|
45
45
|
voiceSilkPath: z.string().optional(),
|
|
46
46
|
voiceSilkArgs: z.array(z.string()).optional(),
|
|
47
|
+
voiceSilkPipe: z.boolean().optional(),
|
|
47
48
|
voiceSampleRate: z.number().int().positive().optional(),
|
|
48
49
|
voiceDecodePath: z.string().optional(),
|
|
49
50
|
voiceDecodeArgs: z.array(z.string()).optional(),
|
package/src/delivery.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { PassThrough } from "node:stream";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
|
|
6
8
|
import type { OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
|
|
@@ -246,6 +248,80 @@ function resolveSilkArgs(params: {
|
|
|
246
248
|
return next;
|
|
247
249
|
}
|
|
248
250
|
|
|
251
|
+
async function encodeSilkWithPipes(params: {
|
|
252
|
+
ffmpegPath: string;
|
|
253
|
+
ffmpegArgs: string[];
|
|
254
|
+
silkPath: string;
|
|
255
|
+
silkArgs: string[];
|
|
256
|
+
timeoutMs: number;
|
|
257
|
+
sampleRate: number;
|
|
258
|
+
}): Promise<{ buffer: Buffer; durationMs: number }> {
|
|
259
|
+
const ffmpeg = spawn(params.ffmpegPath, params.ffmpegArgs, {
|
|
260
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
261
|
+
});
|
|
262
|
+
const silk = spawn(params.silkPath, params.silkArgs, {
|
|
263
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
let pcmBytes = 0;
|
|
267
|
+
const pass = new PassThrough();
|
|
268
|
+
ffmpeg.stdout?.pipe(pass);
|
|
269
|
+
pass.on("data", (chunk) => {
|
|
270
|
+
pcmBytes += chunk.length;
|
|
271
|
+
});
|
|
272
|
+
pass.pipe(silk.stdin!);
|
|
273
|
+
|
|
274
|
+
const silkChunks: Buffer[] = [];
|
|
275
|
+
let ffmpegErr = "";
|
|
276
|
+
let silkErr = "";
|
|
277
|
+
|
|
278
|
+
ffmpeg.stderr?.on("data", (d) => {
|
|
279
|
+
ffmpegErr += d.toString();
|
|
280
|
+
});
|
|
281
|
+
silk.stderr?.on("data", (d) => {
|
|
282
|
+
silkErr += d.toString();
|
|
283
|
+
});
|
|
284
|
+
silk.stdout?.on("data", (d) => {
|
|
285
|
+
silkChunks.push(Buffer.from(d));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const timer = setTimeout(() => {
|
|
289
|
+
ffmpeg.kill("SIGKILL");
|
|
290
|
+
silk.kill("SIGKILL");
|
|
291
|
+
}, params.timeoutMs);
|
|
292
|
+
|
|
293
|
+
const [ffmpegRes, silkRes] = await Promise.all([
|
|
294
|
+
new Promise<{ code: number | null }>((resolve, reject) => {
|
|
295
|
+
ffmpeg.on("error", reject);
|
|
296
|
+
ffmpeg.on("close", (code) => resolve({ code }));
|
|
297
|
+
}),
|
|
298
|
+
new Promise<{ code: number | null }>((resolve, reject) => {
|
|
299
|
+
silk.on("error", reject);
|
|
300
|
+
silk.on("close", (code) => resolve({ code }));
|
|
301
|
+
}),
|
|
302
|
+
]).finally(() => {
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (ffmpegRes.code !== 0) {
|
|
307
|
+
throw new Error(`ffmpeg failed: ${ffmpegErr.trim() || `exit code ${ffmpegRes.code ?? "?"}`}`);
|
|
308
|
+
}
|
|
309
|
+
if (silkRes.code !== 0) {
|
|
310
|
+
throw new Error(`silk encoder failed: ${silkErr.trim() || `exit code ${silkRes.code ?? "?"}`}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const buffer = Buffer.concat(silkChunks);
|
|
314
|
+
if (!buffer.length) {
|
|
315
|
+
throw new Error("silk encoder produced empty output");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const durationMs = Math.max(
|
|
319
|
+
1,
|
|
320
|
+
Math.round((pcmBytes / (params.sampleRate * PCM_BYTES_PER_SAMPLE)) * 1000),
|
|
321
|
+
);
|
|
322
|
+
return { buffer, durationMs };
|
|
323
|
+
}
|
|
324
|
+
|
|
249
325
|
async function convertAudioToSilk(params: {
|
|
250
326
|
account: ResolvedGeweAccount;
|
|
251
327
|
sourcePath: string;
|
|
@@ -278,11 +354,52 @@ async function convertAudioToSilk(params: {
|
|
|
278
354
|
params.account.config.voiceSilkArgs?.length ? [params.account.config.voiceSilkArgs] : [];
|
|
279
355
|
let silkPath = customPath || DEFAULT_VOICE_SILK;
|
|
280
356
|
let argTemplates = customArgs.length ? customArgs : fallbackArgs;
|
|
357
|
+
const pipeEnabled = params.account.config.voiceSilkPipe === true;
|
|
358
|
+
let usePipe = false;
|
|
281
359
|
if (!customPath) {
|
|
282
360
|
const rustSilk = await ensureRustSilkBinary(params.account);
|
|
283
361
|
if (rustSilk) {
|
|
284
362
|
silkPath = rustSilk;
|
|
285
363
|
argTemplates = [rustArgs];
|
|
364
|
+
usePipe = pipeEnabled && customArgs.length === 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (usePipe) {
|
|
369
|
+
try {
|
|
370
|
+
const ffmpegArgs = [
|
|
371
|
+
"-y",
|
|
372
|
+
"-i",
|
|
373
|
+
params.sourcePath,
|
|
374
|
+
"-ac",
|
|
375
|
+
"1",
|
|
376
|
+
"-ar",
|
|
377
|
+
String(sampleRate),
|
|
378
|
+
"-f",
|
|
379
|
+
"s16le",
|
|
380
|
+
"pipe:1",
|
|
381
|
+
];
|
|
382
|
+
const silkArgs = [
|
|
383
|
+
"encode",
|
|
384
|
+
"-i",
|
|
385
|
+
"-",
|
|
386
|
+
"-o",
|
|
387
|
+
"-",
|
|
388
|
+
"--sample-rate",
|
|
389
|
+
String(sampleRate),
|
|
390
|
+
"--tencent",
|
|
391
|
+
"--quiet",
|
|
392
|
+
];
|
|
393
|
+
return await encodeSilkWithPipes({
|
|
394
|
+
ffmpegPath,
|
|
395
|
+
ffmpegArgs,
|
|
396
|
+
silkPath,
|
|
397
|
+
silkArgs,
|
|
398
|
+
timeoutMs: DEFAULT_VOICE_TIMEOUT_MS,
|
|
399
|
+
sampleRate,
|
|
400
|
+
});
|
|
401
|
+
} catch (err) {
|
|
402
|
+
logger.warn?.(`gewe voice convert pipe failed: ${String(err)}`);
|
|
286
403
|
}
|
|
287
404
|
}
|
|
288
405
|
|
|
@@ -497,6 +614,21 @@ async function resolveLinkThumbUrl(params: {
|
|
|
497
614
|
}
|
|
498
615
|
}
|
|
499
616
|
|
|
617
|
+
function normalizeMediaToken(raw: string): string {
|
|
618
|
+
let value = raw.trim();
|
|
619
|
+
if (value.toUpperCase().startsWith("MEDIA:")) {
|
|
620
|
+
value = value.slice("MEDIA:".length).trim();
|
|
621
|
+
}
|
|
622
|
+
if (
|
|
623
|
+
(value.startsWith("`") && value.endsWith("`")) ||
|
|
624
|
+
(value.startsWith("\"") && value.endsWith("\"")) ||
|
|
625
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
626
|
+
) {
|
|
627
|
+
value = value.slice(1, -1).trim();
|
|
628
|
+
}
|
|
629
|
+
return value;
|
|
630
|
+
}
|
|
631
|
+
|
|
500
632
|
async function stageMedia(params: {
|
|
501
633
|
account: ResolvedGeweAccount;
|
|
502
634
|
cfg: OpenClawConfig;
|
|
@@ -504,7 +636,7 @@ async function stageMedia(params: {
|
|
|
504
636
|
allowRemote: boolean;
|
|
505
637
|
}): Promise<ResolvedMedia> {
|
|
506
638
|
const core = getGeweRuntime();
|
|
507
|
-
const rawUrl = params.mediaUrl
|
|
639
|
+
const rawUrl = normalizeMediaToken(params.mediaUrl);
|
|
508
640
|
if (!rawUrl) throw new Error("mediaUrl is empty");
|
|
509
641
|
|
|
510
642
|
if (looksLikeHttpUrl(rawUrl) && params.allowRemote) {
|
|
@@ -598,6 +730,7 @@ export async function deliverGewePayload(params: {
|
|
|
598
730
|
const trimmedText = payload.text?.trim() ?? "";
|
|
599
731
|
const mediaUrl =
|
|
600
732
|
payload.mediaUrl?.trim() || payload.mediaUrls?.[0]?.trim() || "";
|
|
733
|
+
const normalizedMediaUrl = normalizeMediaToken(mediaUrl);
|
|
601
734
|
|
|
602
735
|
if (geweData?.link) {
|
|
603
736
|
const link = geweData.link;
|
|
@@ -625,12 +758,12 @@ export async function deliverGewePayload(params: {
|
|
|
625
758
|
if (mediaUrl) {
|
|
626
759
|
const audioAsVoice = payload.audioAsVoice === true;
|
|
627
760
|
const forceFile = geweData?.forceFile === true;
|
|
628
|
-
const ttsVoiceHint = !forceFile && looksLikeTtsVoiceMediaUrl(
|
|
761
|
+
const ttsVoiceHint = !forceFile && looksLikeTtsVoiceMediaUrl(normalizedMediaUrl);
|
|
629
762
|
const wantsVoice = !forceFile && (audioAsVoice || ttsVoiceHint);
|
|
630
763
|
const staged = await stageMedia({
|
|
631
764
|
account,
|
|
632
765
|
cfg,
|
|
633
|
-
mediaUrl,
|
|
766
|
+
mediaUrl: normalizedMediaUrl,
|
|
634
767
|
allowRemote: !wantsVoice,
|
|
635
768
|
});
|
|
636
769
|
const contentType = staged.contentType;
|
package/src/inbound.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -97,6 +98,71 @@ function resolveDecodeArgs(params: {
|
|
|
97
98
|
return next;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
async function decodeSilkWithPipes(params: {
|
|
102
|
+
silkPath: string;
|
|
103
|
+
silkArgs: string[];
|
|
104
|
+
ffmpegPath: string;
|
|
105
|
+
ffmpegArgs: string[];
|
|
106
|
+
input: Buffer;
|
|
107
|
+
timeoutMs: number;
|
|
108
|
+
}): Promise<Buffer> {
|
|
109
|
+
const silk = spawn(params.silkPath, params.silkArgs, {
|
|
110
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
111
|
+
});
|
|
112
|
+
const ffmpeg = spawn(params.ffmpegPath, params.ffmpegArgs, {
|
|
113
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let silkErr = "";
|
|
117
|
+
let ffmpegErr = "";
|
|
118
|
+
const chunks: Buffer[] = [];
|
|
119
|
+
|
|
120
|
+
silk.stderr?.on("data", (d) => {
|
|
121
|
+
silkErr += d.toString();
|
|
122
|
+
});
|
|
123
|
+
ffmpeg.stderr?.on("data", (d) => {
|
|
124
|
+
ffmpegErr += d.toString();
|
|
125
|
+
});
|
|
126
|
+
ffmpeg.stdout?.on("data", (d) => {
|
|
127
|
+
chunks.push(Buffer.from(d));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
silk.stdout?.pipe(ffmpeg.stdin!);
|
|
131
|
+
silk.stdin?.write(params.input);
|
|
132
|
+
silk.stdin?.end();
|
|
133
|
+
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
silk.kill("SIGKILL");
|
|
136
|
+
ffmpeg.kill("SIGKILL");
|
|
137
|
+
}, params.timeoutMs);
|
|
138
|
+
|
|
139
|
+
const [silkRes, ffmpegRes] = await Promise.all([
|
|
140
|
+
new Promise<{ code: number | null }>((resolve, reject) => {
|
|
141
|
+
silk.on("error", reject);
|
|
142
|
+
silk.on("close", (code) => resolve({ code }));
|
|
143
|
+
}),
|
|
144
|
+
new Promise<{ code: number | null }>((resolve, reject) => {
|
|
145
|
+
ffmpeg.on("error", reject);
|
|
146
|
+
ffmpeg.on("close", (code) => resolve({ code }));
|
|
147
|
+
}),
|
|
148
|
+
]).finally(() => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (silkRes.code !== 0) {
|
|
153
|
+
throw new Error(`silk decode failed: ${silkErr.trim() || `exit code ${silkRes.code ?? "?"}`}`);
|
|
154
|
+
}
|
|
155
|
+
if (ffmpegRes.code !== 0) {
|
|
156
|
+
throw new Error(`ffmpeg failed: ${ffmpegErr.trim() || `exit code ${ffmpegRes.code ?? "?"}`}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const buffer = Buffer.concat(chunks);
|
|
160
|
+
if (!buffer.length) {
|
|
161
|
+
throw new Error("ffmpeg produced empty output");
|
|
162
|
+
}
|
|
163
|
+
return buffer;
|
|
164
|
+
}
|
|
165
|
+
|
|
100
166
|
async function decodeSilkVoice(params: {
|
|
101
167
|
account: ResolvedGeweAccount;
|
|
102
168
|
buffer: Buffer;
|
|
@@ -129,6 +195,52 @@ async function decodeSilkVoice(params: {
|
|
|
129
195
|
];
|
|
130
196
|
if (decodeOutput === "wav") rustArgs.push("--wav");
|
|
131
197
|
const rustSilk = customPath ? null : await ensureRustSilkBinary(params.account);
|
|
198
|
+
const pipeEnabled = params.account.config.voiceSilkPipe === true;
|
|
199
|
+
const usePipe = pipeEnabled && !!rustSilk && !customPath && customArgs.length === 0;
|
|
200
|
+
if (usePipe) {
|
|
201
|
+
try {
|
|
202
|
+
const silkArgs = [
|
|
203
|
+
"decode",
|
|
204
|
+
"-i",
|
|
205
|
+
"-",
|
|
206
|
+
"-o",
|
|
207
|
+
"-",
|
|
208
|
+
"--sample-rate",
|
|
209
|
+
String(sampleRate),
|
|
210
|
+
"--quiet",
|
|
211
|
+
];
|
|
212
|
+
const ffmpegArgs = [
|
|
213
|
+
"-y",
|
|
214
|
+
"-f",
|
|
215
|
+
"s16le",
|
|
216
|
+
"-ar",
|
|
217
|
+
String(sampleRate),
|
|
218
|
+
"-ac",
|
|
219
|
+
"1",
|
|
220
|
+
"-i",
|
|
221
|
+
"pipe:0",
|
|
222
|
+
"-f",
|
|
223
|
+
"wav",
|
|
224
|
+
"pipe:1",
|
|
225
|
+
];
|
|
226
|
+
const buffer = await decodeSilkWithPipes({
|
|
227
|
+
silkPath: rustSilk!,
|
|
228
|
+
silkArgs,
|
|
229
|
+
ffmpegPath,
|
|
230
|
+
ffmpegArgs,
|
|
231
|
+
input: params.buffer,
|
|
232
|
+
timeoutMs: DEFAULT_VOICE_DECODE_TIMEOUT_MS,
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
buffer,
|
|
236
|
+
contentType: "audio/wav",
|
|
237
|
+
fileName: "voice.wav",
|
|
238
|
+
};
|
|
239
|
+
} catch (err) {
|
|
240
|
+
logger.warn?.(`gewe voice decode pipe failed: ${String(err)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
132
244
|
const argTemplates = customArgs.length
|
|
133
245
|
? customArgs
|
|
134
246
|
: rustSilk
|
package/src/silk.ts
CHANGED
|
@@ -30,6 +30,7 @@ type ResolvedVersion = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const installCache = new Map<string, Promise<string | null>>();
|
|
33
|
+
const resolvedPathCache = new Map<string, string | null>();
|
|
33
34
|
|
|
34
35
|
export function buildRustSilkEncodeArgs(params: {
|
|
35
36
|
input: string;
|
|
@@ -94,6 +95,10 @@ export async function ensureRustSilkBinary(
|
|
|
94
95
|
process.arch,
|
|
95
96
|
].join("|");
|
|
96
97
|
|
|
98
|
+
if (resolvedPathCache.has(cacheKey)) {
|
|
99
|
+
return resolvedPathCache.get(cacheKey) ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
if (installCache.has(cacheKey)) {
|
|
98
103
|
return installCache.get(cacheKey) ?? null;
|
|
99
104
|
}
|
|
@@ -106,9 +111,14 @@ export async function ensureRustSilkBinary(
|
|
|
106
111
|
folder,
|
|
107
112
|
isLatest: resolved.isLatest,
|
|
108
113
|
resolvedTag: resolved.resolvedTag,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
})
|
|
115
|
+
.then((result) => {
|
|
116
|
+
resolvedPathCache.set(cacheKey, result);
|
|
117
|
+
return result;
|
|
118
|
+
})
|
|
119
|
+
.finally(() => {
|
|
120
|
+
installCache.delete(cacheKey);
|
|
121
|
+
});
|
|
112
122
|
installCache.set(cacheKey, installPromise);
|
|
113
123
|
return installPromise;
|
|
114
124
|
}
|
package/src/types.ts
CHANGED