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 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.13",
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/download.ts CHANGED
@@ -1,10 +1,55 @@
1
- import { assertGeweOk, postGeweJson } from "./api.js";
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
- function resolveBaseUrl(account: ResolvedGeweAccount): string {
7
- return account.config.apiBaseUrl?.trim() || "https://www.geweapi.com";
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 postGeweJson<DownloadResult>({
16
- baseUrl: resolveBaseUrl(params.account),
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 postGeweJson<DownloadResult>({
36
- baseUrl: resolveBaseUrl(params.account),
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 postGeweJson<DownloadResult>({
55
- baseUrl: resolveBaseUrl(params.account),
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 postGeweJson<DownloadResult>({
73
- baseUrl: resolveBaseUrl(params.account),
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 { resolveGeweAccount } from "./accounts.js";
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: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
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
- if (!account.token || !account.appId) {
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("abort", () => resolve(), { once: true });
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 { resolveGeweAccount, resolveDefaultGeweAccountId, listGeweAccountIds } from "./accounts.js";
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 = Boolean(account.token?.trim() && account.appId?.trim());
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
- "You will need:",
172
- "- GeWe token + appId",
173
- "- Public webhook endpoint (FRP or reverse proxy)",
174
- "- Public media base URL (optional proxy fallback)",
175
- ].join("\n"),
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 = await ctx.prompter.text({
180
- message: "GeWe token",
181
- initialValue: resolved.tokenSource !== "none" ? resolved.token : existing.token,
182
- validate: (value) => (value.trim() ? undefined : "Required"),
183
- });
184
- const appId = await ctx.prompter.text({
185
- message: "GeWe appId",
186
- initialValue: resolved.appIdSource !== "none" ? resolved.appId : existing.appId,
187
- validate: (value) => (value.trim() ? undefined : "Required"),
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 apiBaseUrl = await ctx.prompter.text({
191
- message: "GeWe API base URL",
192
- initialValue: existing.apiBaseUrl ?? DEFAULT_API_BASE_URL,
193
- validate: (value) => (value.trim() ? undefined : "Required"),
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: apiBaseUrl.trim().replace(/\/$/, ""),
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 { assertGeweOk, postGeweJson } from "./api.js";
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
- token: string;
7
- appId: string;
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
- const baseUrl = account.config.apiBaseUrl?.trim() || "https://www.geweapi.com";
12
- return { baseUrl, token: account.token, appId: account.appId };
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 postGeweJson<{
78
+ const resp = await postSendJson<{
41
79
  msgId?: number | string;
42
80
  newMsgId?: number | string;
43
81
  createTime?: number;
44
82
  }>({
45
- baseUrl: ctx.baseUrl,
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 postGeweJson({
66
- baseUrl: ctx.baseUrl,
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 postGeweJson({
87
- baseUrl: ctx.baseUrl,
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 postGeweJson({
110
- baseUrl: ctx.baseUrl,
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 postGeweJson({
133
- baseUrl: ctx.baseUrl,
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 postGeweJson({
157
- baseUrl: ctx.baseUrl,
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;