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 +44 -0
- package/package.json +1 -1
- package/src/accounts.ts +83 -17
- package/src/api.ts +39 -11
- package/src/channel.ts +27 -13
- package/src/config-schema.ts +66 -0
- package/src/delivery.ts +0 -117
- package/src/download.ts +56 -19
- package/src/gateway-client.ts +104 -0
- package/src/inbound.ts +0 -111
- package/src/monitor.ts +88 -4
- package/src/onboarding.ts +130 -27
- package/src/plugin-version.ts +16 -0
- package/src/send.ts +55 -29
- package/src/silk.ts +2 -37
- package/src/state-paths.ts +42 -0
- package/src/types.ts +5 -0
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
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 {
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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:
|
|
54
|
+
body: params.body,
|
|
35
55
|
});
|
|
56
|
+
}
|
|
36
57
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
return
|
|
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 {
|
|
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
|
|
66
|
-
throw new Error("GeWe
|
|
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) =>
|
|
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:
|
|
115
|
+
configured: resolveIsGeweAccountConfigured(account),
|
|
116
|
+
mode: account.mode ?? "direct",
|
|
110
117
|
tokenSource: account.tokenSource,
|
|
111
|
-
baseUrl: account
|
|
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 =
|
|
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
|
|
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: "
|
|
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
|
|
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({
|
package/src/config-schema.ts
CHANGED
|
@@ -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
|
|