gewe-openclaw 2026.3.12 → 2026.3.14

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 CHANGED
@@ -44,6 +44,8 @@ openclaw onboard
44
44
 
45
45
  在通道列表中选择 **GeWe**,按提示填写 `token`、`appId`、`webhook`,以及可选的 `mediaPublicUrl`/`S3` 媒体配置。
46
46
 
47
+ 如果你使用 `gewe-gateway` 网关模式,则改为填写 `gatewayUrl`、`gatewayKey`、`gatewayInstanceId`、`webhookPublicUrl` 和显式群列表。
48
+
47
49
  ### 方式 B:直接编辑配置文件
48
50
 
49
51
  直接编辑 `~/.openclaw/openclaw.json` 的 `channels.gewe-openclaw` 段落(见下方示例)。
@@ -117,6 +119,48 @@ openclaw onboard
117
119
 
118
120
  > 配置变更后需重启 Gateway。
119
121
 
122
+ ## 网关模式
123
+
124
+ 当你没有足够多的微信号,但希望让多台 OpenClaw 临时分别服务不同微信群时,可以把 `gewe-openclaw` 配成网关模式。
125
+
126
+ 网关模式下:
127
+
128
+ - GeWe 官方 webhook 只打到 `gewe-gateway`
129
+ - `gewe-openclaw` 不再直连 GeWe,也不再要求本地配置 `token/appId`
130
+ - 每台 OpenClaw 只声明自己负责的群
131
+ - 网关按群转发入站消息,并统一代理所有出站请求
132
+
133
+ 最小配置示例:
134
+
135
+ ```json5
136
+ {
137
+ "channels": {
138
+ "gewe-openclaw": {
139
+ "enabled": true,
140
+ "gatewayUrl": "https://your-gateway.example.com",
141
+ "gatewayKey": "<gateway-key>",
142
+ "gatewayInstanceId": "openclaw-demo-a",
143
+ "webhookPublicUrl": "https://your-openclaw.example.com/gewe/webhook",
144
+ "webhookSecret": "<callback-secret>",
145
+ "groups": {
146
+ "123456@chatroom": {
147
+ "enabled": true,
148
+ "requireMention": true
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ 网关模式注意事项:
157
+
158
+ - `gatewayUrl` 和 `gatewayKey` 必须成对配置
159
+ - 一旦进入网关模式,`apiBaseUrl` 会被忽略
160
+ - `groups` 必须显式列出群 ID,不能用 `*`
161
+ - `webhookPublicUrl` 必须是网关可访问到的完整回调地址
162
+ - 一个群同一时间只能绑定到一个活跃 OpenClaw 实例
163
+
120
164
  ## 高级用法:让未安装插件也出现在 onboarding 列表
121
165
 
122
166
  默认情况下,**只有已安装的插件**会出现在 onboarding 列表中。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gewe-openclaw",
3
- "version": "2026.3.12",
3
+ "version": "2026.3.14",
4
4
  "type": "module",
5
5
  "description": "OpenClaw GeWe channel plugin",
6
6
  "license": "MIT",
package/src/accounts.ts CHANGED
@@ -3,21 +3,16 @@ import { readFileSync } from "node:fs";
3
3
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
4
4
 
5
5
  import { CHANNEL_CONFIG_KEY } from "./constants.js";
6
- import type { CoreConfig, GeweAccountConfig, GeweAppIdSource, GeweTokenSource } from "./types.js";
6
+ import type {
7
+ CoreConfig,
8
+ GeweAccountConfig,
9
+ GeweAppIdSource,
10
+ GeweTokenSource,
11
+ ResolvedGeweAccount,
12
+ } from "./types.js";
7
13
 
8
14
  const DEFAULT_API_BASE_URL = "https://www.geweapi.com";
9
15
 
10
- export type ResolvedGeweAccount = {
11
- accountId: string;
12
- enabled: boolean;
13
- name?: string;
14
- token: string;
15
- tokenSource: GeweTokenSource;
16
- appId: string;
17
- appIdSource: GeweAppIdSource;
18
- config: GeweAccountConfig;
19
- };
20
-
21
16
  function listConfiguredAccountIds(cfg: CoreConfig): string[] {
22
17
  const accounts = cfg.channels?.[CHANNEL_CONFIG_KEY]?.accounts;
23
18
  if (!accounts || typeof accounts !== "object") return [];
@@ -59,11 +54,24 @@ function mergeGeweAccountConfig(cfg: CoreConfig, accountId: string): GeweAccount
59
54
  return { ...base, ...account };
60
55
  }
61
56
 
57
+ function normalizeUrl(url?: string): string | undefined {
58
+ const trimmed = url?.trim();
59
+ if (!trimmed) return undefined;
60
+ return trimmed.replace(/\/$/, "");
61
+ }
62
+
63
+ function isGatewayModeConfig(config?: Pick<GeweAccountConfig, "gatewayUrl" | "gatewayKey">): boolean {
64
+ return Boolean(normalizeUrl(config?.gatewayUrl) && config?.gatewayKey?.trim());
65
+ }
66
+
62
67
  function resolveToken(
63
68
  cfg: CoreConfig,
64
69
  accountId: string,
65
70
  ): { token: string; source: GeweTokenSource } {
66
71
  const merged = mergeGeweAccountConfig(cfg, accountId);
72
+ if (isGatewayModeConfig(merged)) {
73
+ return { token: "", source: "none" };
74
+ }
67
75
 
68
76
  const envToken = process.env.GEWE_TOKEN?.trim();
69
77
  if (envToken && accountId === DEFAULT_ACCOUNT_ID) {
@@ -91,6 +99,9 @@ function resolveAppId(
91
99
  accountId: string,
92
100
  ): { appId: string; source: GeweAppIdSource } {
93
101
  const merged = mergeGeweAccountConfig(cfg, accountId);
102
+ if (isGatewayModeConfig(merged)) {
103
+ return { appId: "", source: "none" };
104
+ }
94
105
 
95
106
  const envAppId = process.env.GEWE_APP_ID?.trim();
96
107
  if (envAppId && accountId === DEFAULT_ACCOUNT_ID) {
@@ -124,19 +135,18 @@ export function resolveGeweAccount(params: {
124
135
  const merged = mergeGeweAccountConfig(params.cfg, accountId);
125
136
  const accountEnabled = merged.enabled !== false;
126
137
  const enabled = baseEnabled && accountEnabled;
138
+ const mode = isGatewayModeConfig(merged) ? "gateway" : "direct";
127
139
  const tokenResolution = resolveToken(params.cfg, accountId);
128
140
  const appIdResolution = resolveAppId(params.cfg, accountId);
129
141
 
130
- if (!merged.apiBaseUrl) {
131
- merged.apiBaseUrl = DEFAULT_API_BASE_URL;
132
- } else {
133
- merged.apiBaseUrl = merged.apiBaseUrl.trim().replace(/\/$/, "");
134
- }
142
+ merged.apiBaseUrl = normalizeUrl(merged.apiBaseUrl) ?? DEFAULT_API_BASE_URL;
143
+ merged.gatewayUrl = normalizeUrl(merged.gatewayUrl);
135
144
 
136
145
  return {
137
146
  accountId,
138
147
  enabled,
139
148
  name: merged.name?.trim() || undefined,
149
+ mode,
140
150
  token: tokenResolution.token,
141
151
  tokenSource: tokenResolution.source,
142
152
  appId: appIdResolution.appId,
@@ -162,3 +172,59 @@ export function listEnabledGeweAccounts(cfg: CoreConfig): ResolvedGeweAccount[]
162
172
  .map((accountId) => resolveGeweAccount({ cfg, accountId }))
163
173
  .filter((account) => account.enabled);
164
174
  }
175
+
176
+ export function resolveIsGatewayMode(
177
+ account: ResolvedGeweAccount | GeweAccountConfig | null | undefined,
178
+ ): boolean {
179
+ if (!account) return false;
180
+ if ("mode" in account && account.mode) {
181
+ return account.mode === "gateway";
182
+ }
183
+ if ("config" in account && account.config) {
184
+ return isGatewayModeConfig(account.config);
185
+ }
186
+ return isGatewayModeConfig(account);
187
+ }
188
+
189
+ export function resolveIsGeweAccountConfigured(account: ResolvedGeweAccount): boolean {
190
+ if (resolveIsGatewayMode(account)) {
191
+ return Boolean(
192
+ account.config.gatewayUrl?.trim() &&
193
+ account.config.gatewayKey?.trim() &&
194
+ account.config.gatewayInstanceId?.trim() &&
195
+ resolveGatewayCallbackUrl(account) &&
196
+ resolveGatewayGroupBindings(account).length > 0,
197
+ );
198
+ }
199
+ return Boolean(account.token?.trim() && account.appId?.trim());
200
+ }
201
+
202
+ export function resolveGeweTransportBaseUrl(account: ResolvedGeweAccount): string {
203
+ if (resolveIsGatewayMode(account)) {
204
+ return normalizeUrl(account.config.gatewayUrl) ?? DEFAULT_API_BASE_URL;
205
+ }
206
+ return normalizeUrl(account.config.apiBaseUrl) ?? DEFAULT_API_BASE_URL;
207
+ }
208
+
209
+ export function resolveGatewayGroupBindings(account: ResolvedGeweAccount): string[] {
210
+ const groups = account.config.groups;
211
+ if (!groups || typeof groups !== "object") return [];
212
+ return Object.keys(groups).filter((groupId) => {
213
+ const trimmed = groupId.trim();
214
+ if (!trimmed || trimmed === "*") return false;
215
+ const groupConfig = groups[groupId];
216
+ return groupConfig?.enabled !== false;
217
+ });
218
+ }
219
+
220
+ export function resolveGatewayCallbackUrl(account: ResolvedGeweAccount): string | undefined {
221
+ return account.config.webhookPublicUrl?.trim() || undefined;
222
+ }
223
+
224
+ export function resolveGatewayRegisterIntervalMs(account: ResolvedGeweAccount): number {
225
+ const seconds = account.config.gatewayRegisterIntervalSec;
226
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
227
+ return Math.round(seconds * 1000);
228
+ }
229
+ return 60_000;
230
+ }
package/src/api.ts CHANGED
@@ -18,6 +18,26 @@ async function readResponseText(res: Response): Promise<string> {
18
18
  }
19
19
  }
20
20
 
21
+ async function postJson<T>(params: {
22
+ url: string;
23
+ headers: Record<string, string>;
24
+ body: Record<string, unknown>;
25
+ }): Promise<T> {
26
+ const res = await fetch(params.url, {
27
+ method: "POST",
28
+ headers: params.headers,
29
+ body: JSON.stringify(params.body),
30
+ });
31
+
32
+ if (!res.ok) {
33
+ const text = await readResponseText(res);
34
+ const detail = text ? `: ${text}` : "";
35
+ throw new Error(`HTTP request failed (${res.status})${detail}`);
36
+ }
37
+
38
+ return (await res.json()) as T;
39
+ }
40
+
21
41
  export async function postGeweJson<T>(params: {
22
42
  baseUrl: string;
23
43
  token: string;
@@ -25,23 +45,31 @@ export async function postGeweJson<T>(params: {
25
45
  body: Record<string, unknown>;
26
46
  }): Promise<GeweApiResponse<T>> {
27
47
  const url = buildGeweUrl(params.baseUrl, params.path);
28
- const res = await fetch(url, {
29
- method: "POST",
48
+ return postJson<GeweApiResponse<T>>({
49
+ url,
30
50
  headers: {
31
51
  "Content-Type": "application/json",
32
52
  "X-GEWE-TOKEN": params.token,
33
53
  },
34
- body: JSON.stringify(params.body),
54
+ body: params.body,
35
55
  });
56
+ }
36
57
 
37
- if (!res.ok) {
38
- const text = await readResponseText(res);
39
- const detail = text ? `: ${text}` : "";
40
- throw new Error(`GeWe API request failed (${res.status})${detail}`);
41
- }
42
-
43
- const json = (await res.json()) as GeweApiResponse<T>;
44
- return json;
58
+ export async function postGatewayJson<T>(params: {
59
+ baseUrl: string;
60
+ gatewayKey: string;
61
+ path: string;
62
+ body: Record<string, unknown>;
63
+ }): Promise<T> {
64
+ const url = buildGeweUrl(params.baseUrl, params.path);
65
+ return postJson<T>({
66
+ url,
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ "X-GeWe-Gateway-Key": params.gatewayKey,
70
+ },
71
+ body: params.body,
72
+ });
45
73
  }
46
74
 
47
75
  export function assertGeweOk<T>(resp: GeweApiResponse<T>, context: string): T | undefined {
package/src/channel.ts CHANGED
@@ -13,7 +13,13 @@ import {
13
13
  type ChannelSetupInput,
14
14
  } from "openclaw/plugin-sdk";
15
15
 
16
- import { resolveGeweAccount, resolveDefaultGeweAccountId, listGeweAccountIds } from "./accounts.js";
16
+ import {
17
+ listGeweAccountIds,
18
+ resolveDefaultGeweAccountId,
19
+ resolveGeweAccount,
20
+ resolveGeweTransportBaseUrl,
21
+ resolveIsGeweAccountConfigured,
22
+ } from "./accounts.js";
17
23
  import { GeweConfigSchema } from "./config-schema.js";
18
24
  import {
19
25
  CHANNEL_ALIASES,
@@ -62,8 +68,8 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
62
68
  normalizeAllowEntry: (entry) => stripChannelPrefix(entry),
63
69
  notifyApproval: async ({ cfg, id }) => {
64
70
  const account = resolveGeweAccount({ cfg: cfg as CoreConfig });
65
- if (!account.token || !account.appId) {
66
- throw new Error("GeWe token/appId not configured");
71
+ if (!resolveIsGeweAccountConfigured(account)) {
72
+ throw new Error("GeWe account is not configured");
67
73
  }
68
74
  await sendTextGewe({
69
75
  account,
@@ -101,14 +107,15 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
101
107
  accountId,
102
108
  clearBaseFields: ["token", "tokenFile", "appId", "appIdFile", "name"],
103
109
  }),
104
- isConfigured: (account) => Boolean(account.token?.trim() && account.appId?.trim()),
110
+ isConfigured: (account) => resolveIsGeweAccountConfigured(account),
105
111
  describeAccount: (account) => ({
106
112
  accountId: account.accountId,
107
113
  name: account.name,
108
114
  enabled: account.enabled,
109
- configured: Boolean(account.token?.trim() && account.appId?.trim()),
115
+ configured: resolveIsGeweAccountConfigured(account),
116
+ mode: account.mode ?? "direct",
110
117
  tokenSource: account.tokenSource,
111
- baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
118
+ baseUrl: resolveGeweTransportBaseUrl(account),
112
119
  }),
113
120
  resolveAllowFrom: ({ cfg, accountId }) =>
114
121
  (resolveGeweAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map(
@@ -270,6 +277,9 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
270
277
  lastStartAt: null,
271
278
  lastStopAt: null,
272
279
  lastError: null,
280
+ lastGatewayRegisterAt: null,
281
+ lastGatewayHeartbeatAt: null,
282
+ lastGatewayError: null,
273
283
  },
274
284
  buildChannelSummary: ({ snapshot }) => ({
275
285
  configured: snapshot.configured ?? false,
@@ -281,23 +291,29 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
281
291
  lastError: snapshot.lastError ?? null,
282
292
  lastInboundAt: snapshot.lastInboundAt ?? null,
283
293
  lastOutboundAt: snapshot.lastOutboundAt ?? null,
294
+ lastGatewayRegisterAt: snapshot.lastGatewayRegisterAt ?? null,
295
+ lastGatewayHeartbeatAt: snapshot.lastGatewayHeartbeatAt ?? null,
296
+ lastGatewayError: snapshot.lastGatewayError ?? null,
284
297
  }),
285
298
  buildAccountSnapshot: ({ account, runtime }) => {
286
- const configured = Boolean(account.token?.trim() && account.appId?.trim());
299
+ const configured = resolveIsGeweAccountConfigured(account);
287
300
  return {
288
301
  accountId: account.accountId,
289
302
  name: account.name,
290
303
  enabled: account.enabled,
291
304
  configured,
292
305
  tokenSource: account.tokenSource,
293
- baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
306
+ baseUrl: resolveGeweTransportBaseUrl(account),
294
307
  running: runtime?.running ?? false,
295
308
  lastStartAt: runtime?.lastStartAt ?? null,
296
309
  lastStopAt: runtime?.lastStopAt ?? null,
297
310
  lastError: runtime?.lastError ?? null,
298
- mode: "webhook",
311
+ mode: account.mode ?? "direct",
299
312
  lastInboundAt: runtime?.lastInboundAt ?? null,
300
313
  lastOutboundAt: runtime?.lastOutboundAt ?? null,
314
+ lastGatewayRegisterAt: runtime?.lastGatewayRegisterAt ?? null,
315
+ lastGatewayHeartbeatAt: runtime?.lastGatewayHeartbeatAt ?? null,
316
+ lastGatewayError: runtime?.lastGatewayError ?? null,
301
317
  dmPolicy: account.config.dmPolicy ?? "pairing",
302
318
  };
303
319
  },
@@ -305,10 +321,8 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
305
321
  gateway: {
306
322
  startAccount: async (ctx) => {
307
323
  const account = ctx.account;
308
- if (!account.token || !account.appId) {
309
- throw new Error(
310
- `GeWe not configured for account "${account.accountId}" (missing token/appId)`,
311
- );
324
+ if (!resolveIsGeweAccountConfigured(account)) {
325
+ throw new Error(`GeWe not configured for account "${account.accountId}"`);
312
326
  }
313
327
  ctx.log?.info(`[${account.accountId}] starting GeWe webhook server`);
314
328
  const { stop } = await monitorGeweProvider({
@@ -26,6 +26,10 @@ export const GeweAccountSchemaBase = z
26
26
  enabled: z.boolean().optional(),
27
27
  markdown: MarkdownConfigSchema,
28
28
  apiBaseUrl: z.string().optional(),
29
+ gatewayUrl: z.string().optional(),
30
+ gatewayKey: z.string().optional(),
31
+ gatewayInstanceId: z.string().optional(),
32
+ gatewayRegisterIntervalSec: z.number().positive().optional(),
29
33
  token: z.string().optional(),
30
34
  tokenFile: z.string().optional(),
31
35
  appId: z.string().optional(),
@@ -128,6 +132,68 @@ export const GeweAccountSchemaBase = z
128
132
  }
129
133
  }
130
134
  }
135
+
136
+ const gatewayUrl = value.gatewayUrl?.trim();
137
+ const gatewayKey = value.gatewayKey?.trim();
138
+ const gatewayMode = Boolean(gatewayUrl || gatewayKey);
139
+
140
+ if (gatewayMode && (!gatewayUrl || !gatewayKey)) {
141
+ ctx.addIssue({
142
+ code: z.ZodIssueCode.custom,
143
+ path: [gatewayUrl ? "gatewayKey" : "gatewayUrl"],
144
+ message: "gatewayUrl and gatewayKey must be configured together",
145
+ });
146
+ }
147
+
148
+ if (!gatewayMode) return;
149
+
150
+ const instanceId = value.gatewayInstanceId?.trim();
151
+ if (!instanceId) {
152
+ ctx.addIssue({
153
+ code: z.ZodIssueCode.custom,
154
+ path: ["gatewayInstanceId"],
155
+ message: "gatewayInstanceId is required in gateway mode",
156
+ });
157
+ }
158
+
159
+ const callbackUrl = value.webhookPublicUrl?.trim();
160
+ if (!callbackUrl) {
161
+ ctx.addIssue({
162
+ code: z.ZodIssueCode.custom,
163
+ path: ["webhookPublicUrl"],
164
+ message: "webhookPublicUrl is required in gateway mode",
165
+ });
166
+ } else {
167
+ try {
168
+ new URL(callbackUrl);
169
+ } catch {
170
+ ctx.addIssue({
171
+ code: z.ZodIssueCode.custom,
172
+ path: ["webhookPublicUrl"],
173
+ message: "webhookPublicUrl must be a valid URL in gateway mode",
174
+ });
175
+ }
176
+ }
177
+
178
+ const groups = value.groups ?? {};
179
+ const explicitGroups = Object.keys(groups).filter((groupId) => {
180
+ const trimmed = groupId.trim();
181
+ return Boolean(trimmed) && trimmed !== "*";
182
+ });
183
+ if (explicitGroups.length === 0) {
184
+ ctx.addIssue({
185
+ code: z.ZodIssueCode.custom,
186
+ path: ["groups"],
187
+ message: "gateway mode requires explicit group bindings",
188
+ });
189
+ }
190
+ if (Object.prototype.hasOwnProperty.call(groups, "*")) {
191
+ ctx.addIssue({
192
+ code: z.ZodIssueCode.custom,
193
+ path: ["groups", "*"],
194
+ message: 'gateway mode does not allow wildcard group "*"',
195
+ });
196
+ }
131
197
  });
132
198
 
133
199
  export const GeweAccountSchema = GeweAccountSchemaBase.superRefine((value, ctx) => {
package/src/delivery.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { spawn } from "node:child_process";
2
1
  import fs from "node:fs/promises";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
- import { PassThrough } from "node:stream";
6
4
  import { fileURLToPath } from "node:url";
7
5
 
8
6
  import type { OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
@@ -267,80 +265,6 @@ function resolveSilkArgs(params: {
267
265
  return next;
268
266
  }
269
267
 
270
- async function encodeSilkWithPipes(params: {
271
- ffmpegPath: string;
272
- ffmpegArgs: string[];
273
- silkPath: string;
274
- silkArgs: string[];
275
- timeoutMs: number;
276
- sampleRate: number;
277
- }): Promise<{ buffer: Buffer; durationMs: number }> {
278
- const ffmpeg = spawn(params.ffmpegPath, params.ffmpegArgs, {
279
- stdio: ["ignore", "pipe", "pipe"],
280
- });
281
- const silk = spawn(params.silkPath, params.silkArgs, {
282
- stdio: ["pipe", "pipe", "pipe"],
283
- });
284
-
285
- let pcmBytes = 0;
286
- const pass = new PassThrough();
287
- ffmpeg.stdout?.pipe(pass);
288
- pass.on("data", (chunk) => {
289
- pcmBytes += chunk.length;
290
- });
291
- pass.pipe(silk.stdin!);
292
-
293
- const silkChunks: Buffer[] = [];
294
- let ffmpegErr = "";
295
- let silkErr = "";
296
-
297
- ffmpeg.stderr?.on("data", (d) => {
298
- ffmpegErr += d.toString();
299
- });
300
- silk.stderr?.on("data", (d) => {
301
- silkErr += d.toString();
302
- });
303
- silk.stdout?.on("data", (d) => {
304
- silkChunks.push(Buffer.from(d));
305
- });
306
-
307
- const timer = setTimeout(() => {
308
- ffmpeg.kill("SIGKILL");
309
- silk.kill("SIGKILL");
310
- }, params.timeoutMs);
311
-
312
- const [ffmpegRes, silkRes] = await Promise.all([
313
- new Promise<{ code: number | null }>((resolve, reject) => {
314
- ffmpeg.on("error", reject);
315
- ffmpeg.on("close", (code) => resolve({ code }));
316
- }),
317
- new Promise<{ code: number | null }>((resolve, reject) => {
318
- silk.on("error", reject);
319
- silk.on("close", (code) => resolve({ code }));
320
- }),
321
- ]).finally(() => {
322
- clearTimeout(timer);
323
- });
324
-
325
- if (ffmpegRes.code !== 0) {
326
- throw new Error(`ffmpeg failed: ${ffmpegErr.trim() || `exit code ${ffmpegRes.code ?? "?"}`}`);
327
- }
328
- if (silkRes.code !== 0) {
329
- throw new Error(`silk encoder failed: ${silkErr.trim() || `exit code ${silkRes.code ?? "?"}`}`);
330
- }
331
-
332
- const buffer = Buffer.concat(silkChunks);
333
- if (!buffer.length) {
334
- throw new Error("silk encoder produced empty output");
335
- }
336
-
337
- const durationMs = Math.max(
338
- 1,
339
- Math.round((pcmBytes / (params.sampleRate * PCM_BYTES_PER_SAMPLE)) * 1000),
340
- );
341
- return { buffer, durationMs };
342
- }
343
-
344
268
  async function convertAudioToSilk(params: {
345
269
  account: ResolvedGeweAccount;
346
270
  sourcePath: string;
@@ -373,52 +297,11 @@ async function convertAudioToSilk(params: {
373
297
  params.account.config.voiceSilkArgs?.length ? [params.account.config.voiceSilkArgs] : [];
374
298
  let silkPath = customPath || DEFAULT_VOICE_SILK;
375
299
  let argTemplates = customArgs.length ? customArgs : fallbackArgs;
376
- const pipeEnabled = params.account.config.voiceSilkPipe === true;
377
- let usePipe = false;
378
300
  if (!customPath) {
379
301
  const rustSilk = await ensureRustSilkBinary(params.account);
380
302
  if (rustSilk) {
381
303
  silkPath = rustSilk;
382
304
  argTemplates = [rustArgs];
383
- usePipe = pipeEnabled && customArgs.length === 0;
384
- }
385
- }
386
-
387
- if (usePipe) {
388
- try {
389
- const ffmpegArgs = [
390
- "-y",
391
- "-i",
392
- params.sourcePath,
393
- "-ac",
394
- "1",
395
- "-ar",
396
- String(sampleRate),
397
- "-f",
398
- "s16le",
399
- "pipe:1",
400
- ];
401
- const silkArgs = [
402
- "encode",
403
- "-i",
404
- "-",
405
- "-o",
406
- "-",
407
- "--sample-rate",
408
- String(sampleRate),
409
- "--tencent",
410
- "--quiet",
411
- ];
412
- return await encodeSilkWithPipes({
413
- ffmpegPath,
414
- ffmpegArgs,
415
- silkPath,
416
- silkArgs,
417
- timeoutMs: DEFAULT_VOICE_TIMEOUT_MS,
418
- sampleRate,
419
- });
420
- } catch (err) {
421
- logger.warn?.(`gewe voice convert pipe failed: ${String(err)}`);
422
305
  }
423
306
  }
424
307