gewe-openclaw 2026.3.13 → 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/download.ts +56 -19
- package/src/gateway-client.ts +104 -0
- 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/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/download.ts
CHANGED
|
@@ -1,10 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveGeweTransportBaseUrl, resolveIsGatewayMode } from "./accounts.js";
|
|
2
|
+
import { assertGeweOk, postGatewayJson, postGeweJson } from "./api.js";
|
|
2
3
|
import type { ResolvedGeweAccount } from "./types.js";
|
|
3
4
|
|
|
4
5
|
type DownloadResult = { fileUrl: string };
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
type DownloadContext = {
|
|
8
|
+
mode: "direct" | "gateway";
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
gatewayKey?: string;
|
|
12
|
+
appId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function buildContext(account: ResolvedGeweAccount): DownloadContext {
|
|
16
|
+
if (resolveIsGatewayMode(account)) {
|
|
17
|
+
return {
|
|
18
|
+
mode: "gateway",
|
|
19
|
+
baseUrl: resolveGeweTransportBaseUrl(account),
|
|
20
|
+
gatewayKey: account.config.gatewayKey?.trim(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
mode: "direct",
|
|
25
|
+
baseUrl: resolveGeweTransportBaseUrl(account),
|
|
26
|
+
token: account.token,
|
|
27
|
+
appId: account.appId,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function postDownloadJson(params: {
|
|
32
|
+
ctx: DownloadContext;
|
|
33
|
+
path: string;
|
|
34
|
+
body: Record<string, unknown>;
|
|
35
|
+
}): Promise<{ ret: number; msg: string; data?: DownloadResult }> {
|
|
36
|
+
if (params.ctx.mode === "gateway") {
|
|
37
|
+
return postGatewayJson<{ ret: number; msg: string; data?: DownloadResult }>({
|
|
38
|
+
baseUrl: params.ctx.baseUrl,
|
|
39
|
+
gatewayKey: params.ctx.gatewayKey?.trim() ?? "",
|
|
40
|
+
path: params.path,
|
|
41
|
+
body: params.body,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return postGeweJson<DownloadResult>({
|
|
45
|
+
baseUrl: params.ctx.baseUrl,
|
|
46
|
+
token: params.ctx.token?.trim() ?? "",
|
|
47
|
+
path: params.path,
|
|
48
|
+
body: {
|
|
49
|
+
appId: params.ctx.appId,
|
|
50
|
+
...params.body,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
8
53
|
}
|
|
9
54
|
|
|
10
55
|
export async function downloadGeweImage(params: {
|
|
@@ -12,12 +57,10 @@ export async function downloadGeweImage(params: {
|
|
|
12
57
|
xml: string;
|
|
13
58
|
type: 1 | 2 | 3;
|
|
14
59
|
}): Promise<string> {
|
|
15
|
-
const resp = await
|
|
16
|
-
|
|
17
|
-
token: params.account.token,
|
|
60
|
+
const resp = await postDownloadJson({
|
|
61
|
+
ctx: buildContext(params.account),
|
|
18
62
|
path: "/gewe/v2/api/message/downloadImage",
|
|
19
63
|
body: {
|
|
20
|
-
appId: params.account.appId,
|
|
21
64
|
xml: params.xml,
|
|
22
65
|
type: params.type,
|
|
23
66
|
},
|
|
@@ -32,12 +75,10 @@ export async function downloadGeweVoice(params: {
|
|
|
32
75
|
xml: string;
|
|
33
76
|
msgId: number;
|
|
34
77
|
}): Promise<string> {
|
|
35
|
-
const resp = await
|
|
36
|
-
|
|
37
|
-
token: params.account.token,
|
|
78
|
+
const resp = await postDownloadJson({
|
|
79
|
+
ctx: buildContext(params.account),
|
|
38
80
|
path: "/gewe/v2/api/message/downloadVoice",
|
|
39
81
|
body: {
|
|
40
|
-
appId: params.account.appId,
|
|
41
82
|
xml: params.xml,
|
|
42
83
|
msgId: params.msgId,
|
|
43
84
|
},
|
|
@@ -51,12 +92,10 @@ export async function downloadGeweVideo(params: {
|
|
|
51
92
|
account: ResolvedGeweAccount;
|
|
52
93
|
xml: string;
|
|
53
94
|
}): Promise<string> {
|
|
54
|
-
const resp = await
|
|
55
|
-
|
|
56
|
-
token: params.account.token,
|
|
95
|
+
const resp = await postDownloadJson({
|
|
96
|
+
ctx: buildContext(params.account),
|
|
57
97
|
path: "/gewe/v2/api/message/downloadVideo",
|
|
58
98
|
body: {
|
|
59
|
-
appId: params.account.appId,
|
|
60
99
|
xml: params.xml,
|
|
61
100
|
},
|
|
62
101
|
});
|
|
@@ -69,12 +108,10 @@ export async function downloadGeweFile(params: {
|
|
|
69
108
|
account: ResolvedGeweAccount;
|
|
70
109
|
xml: string;
|
|
71
110
|
}): Promise<string> {
|
|
72
|
-
const resp = await
|
|
73
|
-
|
|
74
|
-
token: params.account.token,
|
|
111
|
+
const resp = await postDownloadJson({
|
|
112
|
+
ctx: buildContext(params.account),
|
|
75
113
|
path: "/gewe/v2/api/message/downloadFile",
|
|
76
114
|
body: {
|
|
77
|
-
appId: params.account.appId,
|
|
78
115
|
xml: params.xml,
|
|
79
116
|
},
|
|
80
117
|
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveGatewayCallbackUrl,
|
|
3
|
+
resolveGatewayGroupBindings,
|
|
4
|
+
resolveGeweTransportBaseUrl,
|
|
5
|
+
} from "./accounts.js";
|
|
6
|
+
import { postGatewayJson } from "./api.js";
|
|
7
|
+
import type { ResolvedGeweAccount } from "./types.js";
|
|
8
|
+
|
|
9
|
+
type GatewayControlResponse = {
|
|
10
|
+
ok?: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function requireGatewayField(value: string | undefined, field: string): string {
|
|
15
|
+
const trimmed = value?.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
throw new Error(`GeWe gateway ${field} is required`);
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildGatewayRegistrationBody(params: {
|
|
23
|
+
account: ResolvedGeweAccount;
|
|
24
|
+
pluginVersion: string;
|
|
25
|
+
}) {
|
|
26
|
+
const callbackUrl = resolveGatewayCallbackUrl(params.account);
|
|
27
|
+
const groups = resolveGatewayGroupBindings(params.account);
|
|
28
|
+
return {
|
|
29
|
+
instanceId: requireGatewayField(params.account.config.gatewayInstanceId, "instanceId"),
|
|
30
|
+
callbackUrl: requireGatewayField(callbackUrl, "callbackUrl"),
|
|
31
|
+
callbackSecret: params.account.config.webhookSecret?.trim() ?? "",
|
|
32
|
+
groups,
|
|
33
|
+
pluginVersion: params.pluginVersion,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function postGatewayControl(params: {
|
|
38
|
+
account: ResolvedGeweAccount;
|
|
39
|
+
path: string;
|
|
40
|
+
body: Record<string, unknown>;
|
|
41
|
+
}): Promise<GatewayControlResponse> {
|
|
42
|
+
const baseUrl = resolveGeweTransportBaseUrl(params.account);
|
|
43
|
+
const gatewayKey = requireGatewayField(params.account.config.gatewayKey, "key");
|
|
44
|
+
return postGatewayJson<GatewayControlResponse>({
|
|
45
|
+
baseUrl,
|
|
46
|
+
gatewayKey,
|
|
47
|
+
path: params.path,
|
|
48
|
+
body: params.body,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertGatewayOk(
|
|
53
|
+
response: GatewayControlResponse,
|
|
54
|
+
action: "register" | "heartbeat" | "unregister",
|
|
55
|
+
): GatewayControlResponse {
|
|
56
|
+
if (response.ok === false) {
|
|
57
|
+
throw new Error(response.error?.trim() || `GeWe gateway ${action} failed`);
|
|
58
|
+
}
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function registerGatewayInstance(params: {
|
|
63
|
+
account: ResolvedGeweAccount;
|
|
64
|
+
pluginVersion: string;
|
|
65
|
+
}): Promise<GatewayControlResponse> {
|
|
66
|
+
return assertGatewayOk(
|
|
67
|
+
await postGatewayControl({
|
|
68
|
+
account: params.account,
|
|
69
|
+
path: "/gateway/v1/instances/register",
|
|
70
|
+
body: buildGatewayRegistrationBody(params),
|
|
71
|
+
}),
|
|
72
|
+
"register",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function heartbeatGatewayInstance(params: {
|
|
77
|
+
account: ResolvedGeweAccount;
|
|
78
|
+
}): Promise<GatewayControlResponse> {
|
|
79
|
+
return assertGatewayOk(
|
|
80
|
+
await postGatewayControl({
|
|
81
|
+
account: params.account,
|
|
82
|
+
path: "/gateway/v1/instances/heartbeat",
|
|
83
|
+
body: {
|
|
84
|
+
instanceId: requireGatewayField(params.account.config.gatewayInstanceId, "instanceId"),
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
"heartbeat",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function unregisterGatewayInstance(params: {
|
|
92
|
+
account: ResolvedGeweAccount;
|
|
93
|
+
}): Promise<GatewayControlResponse> {
|
|
94
|
+
return assertGatewayOk(
|
|
95
|
+
await postGatewayControl({
|
|
96
|
+
account: params.account,
|
|
97
|
+
path: "/gateway/v1/instances/unregister",
|
|
98
|
+
body: {
|
|
99
|
+
instanceId: requireGatewayField(params.account.config.gatewayInstanceId, "instanceId"),
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
"unregister",
|
|
103
|
+
);
|
|
104
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -2,11 +2,21 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
|
|
|
2
2
|
|
|
3
3
|
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
resolveGatewayRegisterIntervalMs,
|
|
7
|
+
resolveGeweAccount,
|
|
8
|
+
resolveIsGatewayMode,
|
|
9
|
+
} from "./accounts.js";
|
|
6
10
|
import { GeweDownloadQueue } from "./download-queue.js";
|
|
11
|
+
import {
|
|
12
|
+
heartbeatGatewayInstance,
|
|
13
|
+
registerGatewayInstance,
|
|
14
|
+
unregisterGatewayInstance,
|
|
15
|
+
} from "./gateway-client.js";
|
|
7
16
|
import { createGeweInboundDebouncer } from "./inbound-batch.js";
|
|
8
17
|
import { handleGeweInboundBatch } from "./inbound.js";
|
|
9
18
|
import { createGeweMediaServer, DEFAULT_MEDIA_HOST, DEFAULT_MEDIA_PATH, DEFAULT_MEDIA_PORT } from "./media-server.js";
|
|
19
|
+
import { resolvePluginVersion } from "./plugin-version.js";
|
|
10
20
|
import { getGeweRuntime } from "./runtime.js";
|
|
11
21
|
import type {
|
|
12
22
|
CoreConfig,
|
|
@@ -250,7 +260,13 @@ export type GeweMonitorOptions = {
|
|
|
250
260
|
config?: CoreConfig;
|
|
251
261
|
runtime?: RuntimeEnv;
|
|
252
262
|
abortSignal?: AbortSignal;
|
|
253
|
-
statusSink?: (patch: {
|
|
263
|
+
statusSink?: (patch: {
|
|
264
|
+
lastInboundAt?: number;
|
|
265
|
+
lastOutboundAt?: number;
|
|
266
|
+
lastGatewayRegisterAt?: number;
|
|
267
|
+
lastGatewayHeartbeatAt?: number;
|
|
268
|
+
lastGatewayError?: string | null;
|
|
269
|
+
}) => void;
|
|
254
270
|
};
|
|
255
271
|
|
|
256
272
|
export async function monitorGeweProvider(
|
|
@@ -267,7 +283,8 @@ export async function monitorGeweProvider(
|
|
|
267
283
|
},
|
|
268
284
|
};
|
|
269
285
|
|
|
270
|
-
|
|
286
|
+
const gatewayMode = resolveIsGatewayMode(account);
|
|
287
|
+
if (!gatewayMode && (!account.token || !account.appId)) {
|
|
271
288
|
throw new Error(`GeWe not configured for account "${account.accountId}" (token/appId missing)`);
|
|
272
289
|
}
|
|
273
290
|
|
|
@@ -339,18 +356,85 @@ export async function monitorGeweProvider(
|
|
|
339
356
|
);
|
|
340
357
|
}
|
|
341
358
|
|
|
359
|
+
let gatewayHeartbeatTimer: ReturnType<typeof setInterval> | undefined;
|
|
360
|
+
let gatewayStopped = false;
|
|
361
|
+
const stopGatewayRegistration = () => {
|
|
362
|
+
if (gatewayStopped) return;
|
|
363
|
+
gatewayStopped = true;
|
|
364
|
+
if (gatewayHeartbeatTimer) {
|
|
365
|
+
clearInterval(gatewayHeartbeatTimer);
|
|
366
|
+
gatewayHeartbeatTimer = undefined;
|
|
367
|
+
}
|
|
368
|
+
if (!gatewayMode) return;
|
|
369
|
+
void unregisterGatewayInstance({ account }).catch((err) => {
|
|
370
|
+
runtime.error?.(`[${account.accountId}] GeWe gateway unregister failed: ${String(err)}`);
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
if (gatewayMode) {
|
|
375
|
+
const pluginVersion = resolvePluginVersion();
|
|
376
|
+
await registerGatewayInstance({ account, pluginVersion });
|
|
377
|
+
opts.statusSink?.({
|
|
378
|
+
lastGatewayRegisterAt: Date.now(),
|
|
379
|
+
lastGatewayError: null,
|
|
380
|
+
});
|
|
381
|
+
runtime.log?.(
|
|
382
|
+
`[${account.accountId}] registered GeWe gateway instance ${account.config.gatewayInstanceId?.trim() || account.accountId}`,
|
|
383
|
+
);
|
|
384
|
+
const intervalMs = resolveGatewayRegisterIntervalMs(account);
|
|
385
|
+
gatewayHeartbeatTimer = setInterval(() => {
|
|
386
|
+
void heartbeatGatewayInstance({ account })
|
|
387
|
+
.then(() => {
|
|
388
|
+
opts.statusSink?.({
|
|
389
|
+
lastGatewayHeartbeatAt: Date.now(),
|
|
390
|
+
lastGatewayError: null,
|
|
391
|
+
});
|
|
392
|
+
})
|
|
393
|
+
.catch(async (heartbeatError) => {
|
|
394
|
+
const message = String(heartbeatError);
|
|
395
|
+
opts.statusSink?.({ lastGatewayError: message });
|
|
396
|
+
runtime.error?.(
|
|
397
|
+
`[${account.accountId}] GeWe gateway heartbeat failed: ${message}`,
|
|
398
|
+
);
|
|
399
|
+
try {
|
|
400
|
+
await registerGatewayInstance({ account, pluginVersion });
|
|
401
|
+
opts.statusSink?.({
|
|
402
|
+
lastGatewayRegisterAt: Date.now(),
|
|
403
|
+
lastGatewayError: null,
|
|
404
|
+
});
|
|
405
|
+
runtime.log?.(`[${account.accountId}] re-registered GeWe gateway instance`);
|
|
406
|
+
} catch (registerError) {
|
|
407
|
+
const registerMessage = String(registerError);
|
|
408
|
+
opts.statusSink?.({ lastGatewayError: registerMessage });
|
|
409
|
+
runtime.error?.(
|
|
410
|
+
`[${account.accountId}] GeWe gateway re-register failed: ${registerMessage}`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}, intervalMs);
|
|
415
|
+
}
|
|
416
|
+
|
|
342
417
|
let resolveRunning: (() => void) | undefined;
|
|
343
418
|
const runningPromise = new Promise<void>((resolve) => {
|
|
344
419
|
resolveRunning = resolve;
|
|
345
420
|
if (!opts.abortSignal) return;
|
|
346
421
|
if (opts.abortSignal.aborted) {
|
|
422
|
+
stopGatewayRegistration();
|
|
347
423
|
resolve();
|
|
348
424
|
return;
|
|
349
425
|
}
|
|
350
|
-
opts.abortSignal.addEventListener(
|
|
426
|
+
opts.abortSignal.addEventListener(
|
|
427
|
+
"abort",
|
|
428
|
+
() => {
|
|
429
|
+
stopGatewayRegistration();
|
|
430
|
+
resolve();
|
|
431
|
+
},
|
|
432
|
+
{ once: true },
|
|
433
|
+
);
|
|
351
434
|
});
|
|
352
435
|
|
|
353
436
|
const stop = () => {
|
|
437
|
+
stopGatewayRegistration();
|
|
354
438
|
void debouncer.flushAll();
|
|
355
439
|
webhookServer.stop();
|
|
356
440
|
if (mediaStop) mediaStop();
|
package/src/onboarding.ts
CHANGED
|
@@ -2,7 +2,12 @@ import type { ChannelPlugin, OpenClawConfig, WizardPrompter } from "openclaw/plu
|
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
|
3
3
|
|
|
4
4
|
import type { CoreConfig, GeweAccountConfig, ResolvedGeweAccount } from "./types.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
listGeweAccountIds,
|
|
7
|
+
resolveDefaultGeweAccountId,
|
|
8
|
+
resolveGeweAccount,
|
|
9
|
+
resolveIsGeweAccountConfigured,
|
|
10
|
+
} from "./accounts.js";
|
|
6
11
|
import { CHANNEL_CONFIG_KEY, CHANNEL_ID, stripChannelPrefix } from "./constants.js";
|
|
7
12
|
|
|
8
13
|
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
@@ -69,6 +74,14 @@ function parseAllowFrom(raw: string): string[] {
|
|
|
69
74
|
.filter(Boolean);
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
function parseGroupIds(raw: string): string[] {
|
|
78
|
+
return raw
|
|
79
|
+
.split(/[\n,;]+/g)
|
|
80
|
+
.map((entry) => entry.trim())
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.filter((entry) => entry !== "*");
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
async function promptAllowFrom(params: {
|
|
73
86
|
prompter: WizardPrompter;
|
|
74
87
|
existing?: Array<string | number>;
|
|
@@ -148,9 +161,9 @@ export const geweOnboarding: GeweOnboardingAdapter = {
|
|
|
148
161
|
ctx.accountOverrides?.[CHANNEL_ID] ??
|
|
149
162
|
resolveDefaultGeweAccountId(ctx.cfg as CoreConfig);
|
|
150
163
|
const account = resolveGeweAccount({ cfg: ctx.cfg as CoreConfig, accountId });
|
|
151
|
-
const configured =
|
|
164
|
+
const configured = resolveIsGeweAccountConfigured(account);
|
|
152
165
|
const label = configured ? "configured" : "not configured";
|
|
153
|
-
const status = `GeWe (${accountId}): ${label}`;
|
|
166
|
+
const status = `GeWe (${accountId}, ${account.mode ?? "direct"}): ${label}`;
|
|
154
167
|
return {
|
|
155
168
|
channel: CHANNEL_ID,
|
|
156
169
|
configured,
|
|
@@ -165,33 +178,83 @@ export const geweOnboarding: GeweOnboardingAdapter = {
|
|
|
165
178
|
: resolveDefaultGeweAccountId(ctx.cfg as CoreConfig);
|
|
166
179
|
const resolved = resolveGeweAccount({ cfg: ctx.cfg as CoreConfig, accountId });
|
|
167
180
|
const existing = readAccountConfig(ctx.cfg, accountId);
|
|
181
|
+
const mode = await ctx.prompter.select({
|
|
182
|
+
message: "Connection mode",
|
|
183
|
+
options: [
|
|
184
|
+
{ value: "direct", label: "Direct GeWe API" },
|
|
185
|
+
{ value: "gateway", label: "Gateway mode" },
|
|
186
|
+
],
|
|
187
|
+
initialValue: resolved.mode ?? "direct",
|
|
188
|
+
});
|
|
168
189
|
|
|
169
190
|
await ctx.prompter.note(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
mode === "gateway"
|
|
192
|
+
? [
|
|
193
|
+
"You will need:",
|
|
194
|
+
"- Gateway URL + gateway key",
|
|
195
|
+
"- Gateway instance id",
|
|
196
|
+
"- Public webhook endpoint reachable by the gateway",
|
|
197
|
+
"- Explicit WeChat group ids (chatroom ids) for routing",
|
|
198
|
+
"- Public media base URL (optional proxy fallback)",
|
|
199
|
+
].join("\n")
|
|
200
|
+
: [
|
|
201
|
+
"You will need:",
|
|
202
|
+
"- GeWe token + appId",
|
|
203
|
+
"- Public webhook endpoint (FRP or reverse proxy)",
|
|
204
|
+
"- Public media base URL (optional proxy fallback)",
|
|
205
|
+
].join("\n"),
|
|
176
206
|
"GeWe setup",
|
|
177
207
|
);
|
|
178
208
|
|
|
179
|
-
const token =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
const token =
|
|
210
|
+
mode === "direct"
|
|
211
|
+
? await ctx.prompter.text({
|
|
212
|
+
message: "GeWe token",
|
|
213
|
+
initialValue: resolved.tokenSource !== "none" ? resolved.token : existing.token,
|
|
214
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
215
|
+
})
|
|
216
|
+
: "";
|
|
217
|
+
const appId =
|
|
218
|
+
mode === "direct"
|
|
219
|
+
? await ctx.prompter.text({
|
|
220
|
+
message: "GeWe appId",
|
|
221
|
+
initialValue: resolved.appIdSource !== "none" ? resolved.appId : existing.appId,
|
|
222
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
223
|
+
})
|
|
224
|
+
: "";
|
|
189
225
|
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
226
|
+
const gatewayUrl =
|
|
227
|
+
mode === "gateway"
|
|
228
|
+
? await ctx.prompter.text({
|
|
229
|
+
message: "Gateway URL",
|
|
230
|
+
initialValue: existing.gatewayUrl,
|
|
231
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
232
|
+
})
|
|
233
|
+
: "";
|
|
234
|
+
const gatewayKey =
|
|
235
|
+
mode === "gateway"
|
|
236
|
+
? await ctx.prompter.text({
|
|
237
|
+
message: "Gateway key",
|
|
238
|
+
initialValue: existing.gatewayKey,
|
|
239
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
240
|
+
})
|
|
241
|
+
: "";
|
|
242
|
+
const gatewayInstanceId =
|
|
243
|
+
mode === "gateway"
|
|
244
|
+
? await ctx.prompter.text({
|
|
245
|
+
message: "Gateway instance id",
|
|
246
|
+
initialValue: existing.gatewayInstanceId ?? accountId,
|
|
247
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
248
|
+
})
|
|
249
|
+
: "";
|
|
250
|
+
const apiBaseUrl =
|
|
251
|
+
mode === "direct"
|
|
252
|
+
? await ctx.prompter.text({
|
|
253
|
+
message: "GeWe API base URL",
|
|
254
|
+
initialValue: existing.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
255
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
256
|
+
})
|
|
257
|
+
: "";
|
|
195
258
|
|
|
196
259
|
const webhookHost = await ctx.prompter.text({
|
|
197
260
|
message: "Webhook host",
|
|
@@ -212,6 +275,31 @@ export const geweOnboarding: GeweOnboardingAdapter = {
|
|
|
212
275
|
initialValue: existing.webhookPath ?? DEFAULT_WEBHOOK_PATH,
|
|
213
276
|
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
214
277
|
});
|
|
278
|
+
const webhookPublicUrl =
|
|
279
|
+
mode === "gateway"
|
|
280
|
+
? await ctx.prompter.text({
|
|
281
|
+
message: "Webhook public URL",
|
|
282
|
+
placeholder: "https://openclaw.example.com/webhook",
|
|
283
|
+
initialValue: existing.webhookPublicUrl,
|
|
284
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
285
|
+
})
|
|
286
|
+
: existing.webhookPublicUrl;
|
|
287
|
+
const gatewayGroups =
|
|
288
|
+
mode === "gateway"
|
|
289
|
+
? parseGroupIds(
|
|
290
|
+
String(
|
|
291
|
+
await ctx.prompter.text({
|
|
292
|
+
message: "Gateway group ids (comma or newline separated)",
|
|
293
|
+
placeholder: "123456@chatroom",
|
|
294
|
+
initialValue: Object.keys(existing.groups ?? {})
|
|
295
|
+
.filter((groupId) => groupId !== "*")
|
|
296
|
+
.join(", "),
|
|
297
|
+
validate: (value) =>
|
|
298
|
+
parseGroupIds(value).length > 0 ? undefined : "At least one group is required",
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
: [];
|
|
215
303
|
|
|
216
304
|
const mediaPublicUrl = await ctx.prompter.text({
|
|
217
305
|
message: "Media public URL (prefix)",
|
|
@@ -346,16 +434,31 @@ export const geweOnboarding: GeweOnboardingAdapter = {
|
|
|
346
434
|
|
|
347
435
|
let nextCfg = applyAccountPatch(ctx.cfg, accountId, {
|
|
348
436
|
enabled: true,
|
|
349
|
-
token: token.trim(),
|
|
350
|
-
appId: appId.trim(),
|
|
351
|
-
apiBaseUrl:
|
|
437
|
+
token: mode === "direct" ? token.trim() : undefined,
|
|
438
|
+
appId: mode === "direct" ? appId.trim() : undefined,
|
|
439
|
+
apiBaseUrl:
|
|
440
|
+
mode === "direct" ? apiBaseUrl.trim().replace(/\/$/, "") : undefined,
|
|
441
|
+
gatewayUrl: mode === "gateway" ? gatewayUrl.trim().replace(/\/$/, "") : undefined,
|
|
442
|
+
gatewayKey: mode === "gateway" ? gatewayKey.trim() : undefined,
|
|
443
|
+
gatewayInstanceId: mode === "gateway" ? gatewayInstanceId.trim() : undefined,
|
|
352
444
|
webhookHost: webhookHost.trim(),
|
|
353
445
|
webhookPort: Number(webhookPortRaw),
|
|
354
446
|
webhookPath: webhookPath.trim(),
|
|
447
|
+
webhookPublicUrl:
|
|
448
|
+
mode === "gateway" ? webhookPublicUrl?.trim() || undefined : existing.webhookPublicUrl,
|
|
355
449
|
mediaHost: existing.mediaHost ?? DEFAULT_MEDIA_HOST,
|
|
356
450
|
mediaPort: existing.mediaPort ?? DEFAULT_MEDIA_PORT,
|
|
357
451
|
mediaPath: existing.mediaPath ?? DEFAULT_MEDIA_PATH,
|
|
358
452
|
mediaPublicUrl: mediaPublicUrl.trim() || undefined,
|
|
453
|
+
groups:
|
|
454
|
+
mode === "gateway"
|
|
455
|
+
? Object.fromEntries(
|
|
456
|
+
gatewayGroups.map((groupId) => [
|
|
457
|
+
groupId,
|
|
458
|
+
existing.groups?.[groupId] ?? { enabled: true },
|
|
459
|
+
]),
|
|
460
|
+
)
|
|
461
|
+
: existing.groups,
|
|
359
462
|
...s3Patch,
|
|
360
463
|
...(allowFrom ? { allowFrom } : {}),
|
|
361
464
|
...(dmPolicy ? { dmPolicy } : {}),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
let cachedVersion: string | undefined;
|
|
4
|
+
|
|
5
|
+
export function resolvePluginVersion(): string {
|
|
6
|
+
if (cachedVersion) return cachedVersion;
|
|
7
|
+
try {
|
|
8
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
9
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
10
|
+
const version = parsed.version?.trim();
|
|
11
|
+
cachedVersion = version || "0.0.0";
|
|
12
|
+
} catch {
|
|
13
|
+
cachedVersion = "0.0.0";
|
|
14
|
+
}
|
|
15
|
+
return cachedVersion;
|
|
16
|
+
}
|
package/src/send.ts
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveGeweTransportBaseUrl, resolveIsGatewayMode } from "./accounts.js";
|
|
2
|
+
import { assertGeweOk, postGatewayJson, postGeweJson } from "./api.js";
|
|
2
3
|
import type { GeweSendResult, ResolvedGeweAccount } from "./types.js";
|
|
3
4
|
|
|
4
5
|
type GeweSendContext = {
|
|
5
6
|
baseUrl: string;
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
mode: "direct" | "gateway";
|
|
8
|
+
token?: string;
|
|
9
|
+
gatewayKey?: string;
|
|
10
|
+
appId?: string;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
function buildContext(account: ResolvedGeweAccount): GeweSendContext {
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
if (resolveIsGatewayMode(account)) {
|
|
15
|
+
return {
|
|
16
|
+
mode: "gateway",
|
|
17
|
+
baseUrl: resolveGeweTransportBaseUrl(account),
|
|
18
|
+
gatewayKey: account.config.gatewayKey?.trim(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
mode: "direct",
|
|
23
|
+
baseUrl: resolveGeweTransportBaseUrl(account),
|
|
24
|
+
token: account.token,
|
|
25
|
+
appId: account.appId,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function postSendJson<T>(params: {
|
|
30
|
+
ctx: GeweSendContext;
|
|
31
|
+
path: string;
|
|
32
|
+
body: Record<string, unknown>;
|
|
33
|
+
}): Promise<{ ret: number; msg: string; data?: T }> {
|
|
34
|
+
if (params.ctx.mode === "gateway") {
|
|
35
|
+
return postGatewayJson<{ ret: number; msg: string; data?: T }>({
|
|
36
|
+
baseUrl: params.ctx.baseUrl,
|
|
37
|
+
gatewayKey: params.ctx.gatewayKey?.trim() ?? "",
|
|
38
|
+
path: params.path,
|
|
39
|
+
body: params.body,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return postGeweJson<T>({
|
|
43
|
+
baseUrl: params.ctx.baseUrl,
|
|
44
|
+
token: params.ctx.token?.trim() ?? "",
|
|
45
|
+
path: params.path,
|
|
46
|
+
body: {
|
|
47
|
+
appId: params.ctx.appId,
|
|
48
|
+
...params.body,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
13
51
|
}
|
|
14
52
|
|
|
15
53
|
function resolveSendResult(params: {
|
|
@@ -37,16 +75,14 @@ export async function sendTextGewe(params: {
|
|
|
37
75
|
ats?: string;
|
|
38
76
|
}): Promise<GeweSendResult> {
|
|
39
77
|
const ctx = buildContext(params.account);
|
|
40
|
-
const resp = await
|
|
78
|
+
const resp = await postSendJson<{
|
|
41
79
|
msgId?: number | string;
|
|
42
80
|
newMsgId?: number | string;
|
|
43
81
|
createTime?: number;
|
|
44
82
|
}>({
|
|
45
|
-
|
|
46
|
-
token: ctx.token,
|
|
83
|
+
ctx,
|
|
47
84
|
path: "/gewe/v2/api/message/postText",
|
|
48
85
|
body: {
|
|
49
|
-
appId: ctx.appId,
|
|
50
86
|
toWxid: params.toWxid,
|
|
51
87
|
content: params.content,
|
|
52
88
|
...(params.ats ? { ats: params.ats } : {}),
|
|
@@ -62,12 +98,10 @@ export async function sendImageGewe(params: {
|
|
|
62
98
|
imgUrl: string;
|
|
63
99
|
}): Promise<GeweSendResult> {
|
|
64
100
|
const ctx = buildContext(params.account);
|
|
65
|
-
const resp = await
|
|
66
|
-
|
|
67
|
-
token: ctx.token,
|
|
101
|
+
const resp = await postSendJson({
|
|
102
|
+
ctx,
|
|
68
103
|
path: "/gewe/v2/api/message/postImage",
|
|
69
104
|
body: {
|
|
70
|
-
appId: ctx.appId,
|
|
71
105
|
toWxid: params.toWxid,
|
|
72
106
|
imgUrl: params.imgUrl,
|
|
73
107
|
},
|
|
@@ -83,12 +117,10 @@ export async function sendVoiceGewe(params: {
|
|
|
83
117
|
voiceDuration: number;
|
|
84
118
|
}): Promise<GeweSendResult> {
|
|
85
119
|
const ctx = buildContext(params.account);
|
|
86
|
-
const resp = await
|
|
87
|
-
|
|
88
|
-
token: ctx.token,
|
|
120
|
+
const resp = await postSendJson({
|
|
121
|
+
ctx,
|
|
89
122
|
path: "/gewe/v2/api/message/postVoice",
|
|
90
123
|
body: {
|
|
91
|
-
appId: ctx.appId,
|
|
92
124
|
toWxid: params.toWxid,
|
|
93
125
|
voiceUrl: params.voiceUrl,
|
|
94
126
|
voiceDuration: params.voiceDuration,
|
|
@@ -106,12 +138,10 @@ export async function sendVideoGewe(params: {
|
|
|
106
138
|
videoDuration: number;
|
|
107
139
|
}): Promise<GeweSendResult> {
|
|
108
140
|
const ctx = buildContext(params.account);
|
|
109
|
-
const resp = await
|
|
110
|
-
|
|
111
|
-
token: ctx.token,
|
|
141
|
+
const resp = await postSendJson({
|
|
142
|
+
ctx,
|
|
112
143
|
path: "/gewe/v2/api/message/postVideo",
|
|
113
144
|
body: {
|
|
114
|
-
appId: ctx.appId,
|
|
115
145
|
toWxid: params.toWxid,
|
|
116
146
|
videoUrl: params.videoUrl,
|
|
117
147
|
thumbUrl: params.thumbUrl,
|
|
@@ -129,12 +159,10 @@ export async function sendFileGewe(params: {
|
|
|
129
159
|
fileName: string;
|
|
130
160
|
}): Promise<GeweSendResult> {
|
|
131
161
|
const ctx = buildContext(params.account);
|
|
132
|
-
const resp = await
|
|
133
|
-
|
|
134
|
-
token: ctx.token,
|
|
162
|
+
const resp = await postSendJson({
|
|
163
|
+
ctx,
|
|
135
164
|
path: "/gewe/v2/api/message/postFile",
|
|
136
165
|
body: {
|
|
137
|
-
appId: ctx.appId,
|
|
138
166
|
toWxid: params.toWxid,
|
|
139
167
|
fileUrl: params.fileUrl,
|
|
140
168
|
fileName: params.fileName,
|
|
@@ -153,12 +181,10 @@ export async function sendLinkGewe(params: {
|
|
|
153
181
|
thumbUrl: string;
|
|
154
182
|
}): Promise<GeweSendResult> {
|
|
155
183
|
const ctx = buildContext(params.account);
|
|
156
|
-
const resp = await
|
|
157
|
-
|
|
158
|
-
token: ctx.token,
|
|
184
|
+
const resp = await postSendJson({
|
|
185
|
+
ctx,
|
|
159
186
|
path: "/gewe/v2/api/message/postLink",
|
|
160
187
|
body: {
|
|
161
|
-
appId: ctx.appId,
|
|
162
188
|
toWxid: params.toWxid,
|
|
163
189
|
title: params.title,
|
|
164
190
|
desc: params.desc,
|
package/src/types.ts
CHANGED
|
@@ -19,6 +19,10 @@ export type GeweAccountConfig = {
|
|
|
19
19
|
name?: string;
|
|
20
20
|
enabled?: boolean;
|
|
21
21
|
apiBaseUrl?: string;
|
|
22
|
+
gatewayUrl?: string;
|
|
23
|
+
gatewayKey?: string;
|
|
24
|
+
gatewayInstanceId?: string;
|
|
25
|
+
gatewayRegisterIntervalSec?: number;
|
|
22
26
|
token?: string;
|
|
23
27
|
tokenFile?: string;
|
|
24
28
|
appId?: string;
|
|
@@ -99,6 +103,7 @@ export type ResolvedGeweAccount = {
|
|
|
99
103
|
accountId: string;
|
|
100
104
|
name?: string;
|
|
101
105
|
enabled: boolean;
|
|
106
|
+
mode?: "direct" | "gateway";
|
|
102
107
|
token: string;
|
|
103
108
|
tokenSource: GeweTokenSource;
|
|
104
109
|
appId: string;
|