gewe-openclaw 2026.3.13 → 2026.3.23
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 +455 -3
- package/index.ts +39 -1
- package/package.json +12 -1
- package/skills/gewe-agent-tools/SKILL.md +113 -0
- package/skills/gewe-channel-rules/SKILL.md +7 -0
- package/src/accounts.ts +51 -5
- package/src/api-tools.ts +1264 -0
- package/src/api.ts +37 -2
- package/src/binary-command.ts +65 -0
- package/src/channel-actions.ts +536 -0
- package/src/channel-allowlist.ts +150 -0
- package/src/channel-directory.ts +419 -0
- package/src/channel-status.ts +186 -0
- package/src/channel.ts +155 -58
- package/src/config-edit.ts +94 -0
- package/src/config-schema.ts +78 -3
- package/src/contacts-api.ts +113 -0
- package/src/delivery.ts +502 -62
- package/src/directory-cache.ts +164 -0
- package/src/gewe-account-api.ts +27 -0
- package/src/group-allowlist-tool.ts +242 -0
- package/src/group-binding-tool.ts +154 -0
- package/src/group-binding.ts +405 -0
- package/src/groups-api.ts +146 -0
- package/src/inbound-batch.ts +5 -2
- package/src/inbound.ts +248 -41
- package/src/media-server.ts +73 -93
- package/src/moments-api.ts +138 -0
- package/src/monitor.ts +81 -24
- package/src/onboarding.ts +9 -4
- package/src/openclaw-compat.ts +1070 -0
- package/src/pairing-store.ts +478 -0
- package/src/personal-api.ts +45 -0
- package/src/policy.ts +130 -22
- package/src/quote-context-cache.ts +97 -0
- package/src/reply-options.ts +101 -2
- package/src/s3.ts +1 -1
- package/src/send.ts +235 -16
- package/src/setup-wizard-types.ts +162 -0
- package/src/setup-wizard.ts +464 -0
- package/src/silk.ts +2 -1
- package/src/state-paths.ts +55 -14
- package/src/types.ts +66 -7
- package/src/xml.ts +158 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "./openclaw-compat.js";
|
|
2
|
+
|
|
3
|
+
import { listGeweAccountIds, resolveGeweAccount } from "./accounts.js";
|
|
4
|
+
import { CHANNEL_CONFIG_KEY, CHANNEL_ID, stripChannelPrefix } from "./constants.js";
|
|
5
|
+
import type { GeweSetupWizard } from "./setup-wizard-types.js";
|
|
6
|
+
import type { CoreConfig, GeweAccountConfig } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
9
|
+
const DEFAULT_WEBHOOK_PORT = 4399;
|
|
10
|
+
const DEFAULT_WEBHOOK_PATH = "/webhook";
|
|
11
|
+
const DEFAULT_MEDIA_HOST = "0.0.0.0";
|
|
12
|
+
const DEFAULT_MEDIA_PORT = 4400;
|
|
13
|
+
const DEFAULT_MEDIA_PATH = "/gewe-media";
|
|
14
|
+
const DEFAULT_API_BASE_URL = "https://www.geweapi.com";
|
|
15
|
+
|
|
16
|
+
type ChannelSection = GeweAccountConfig & {
|
|
17
|
+
accounts?: Record<string, GeweAccountConfig>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function readChannelSection(cfg: OpenClawConfig): ChannelSection {
|
|
21
|
+
return (cfg.channels?.[CHANNEL_CONFIG_KEY] ?? {}) as ChannelSection;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readAccountConfig(cfg: OpenClawConfig, accountId: string): GeweAccountConfig {
|
|
25
|
+
const section = readChannelSection(cfg);
|
|
26
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
27
|
+
return section;
|
|
28
|
+
}
|
|
29
|
+
return section.accounts?.[accountId] ?? {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setGeweAccountConfig(
|
|
33
|
+
cfg: OpenClawConfig,
|
|
34
|
+
accountId: string,
|
|
35
|
+
patch: Partial<GeweAccountConfig>,
|
|
36
|
+
): OpenClawConfig {
|
|
37
|
+
const section = readChannelSection(cfg);
|
|
38
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
39
|
+
return {
|
|
40
|
+
...cfg,
|
|
41
|
+
channels: {
|
|
42
|
+
...cfg.channels,
|
|
43
|
+
[CHANNEL_CONFIG_KEY]: {
|
|
44
|
+
...section,
|
|
45
|
+
enabled: true,
|
|
46
|
+
...patch,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...cfg,
|
|
54
|
+
channels: {
|
|
55
|
+
...cfg.channels,
|
|
56
|
+
[CHANNEL_CONFIG_KEY]: {
|
|
57
|
+
...section,
|
|
58
|
+
enabled: true,
|
|
59
|
+
accounts: {
|
|
60
|
+
...(section.accounts ?? {}),
|
|
61
|
+
[accountId]: {
|
|
62
|
+
...(section.accounts?.[accountId] ?? {}),
|
|
63
|
+
enabled: true,
|
|
64
|
+
...patch,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clearGeweAccountFields(
|
|
73
|
+
cfg: OpenClawConfig,
|
|
74
|
+
accountId: string,
|
|
75
|
+
fields: Array<keyof GeweAccountConfig>,
|
|
76
|
+
): OpenClawConfig {
|
|
77
|
+
const section = readChannelSection(cfg);
|
|
78
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
79
|
+
const next = { ...section } as ChannelSection;
|
|
80
|
+
for (const field of fields) {
|
|
81
|
+
delete next[field];
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...cfg,
|
|
85
|
+
channels: {
|
|
86
|
+
...cfg.channels,
|
|
87
|
+
[CHANNEL_CONFIG_KEY]: next,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const accounts = { ...(section.accounts ?? {}) };
|
|
93
|
+
const entry = { ...(accounts[accountId] ?? {}) };
|
|
94
|
+
for (const field of fields) {
|
|
95
|
+
delete entry[field];
|
|
96
|
+
}
|
|
97
|
+
accounts[accountId] = entry;
|
|
98
|
+
return {
|
|
99
|
+
...cfg,
|
|
100
|
+
channels: {
|
|
101
|
+
...cfg.channels,
|
|
102
|
+
[CHANNEL_CONFIG_KEY]: {
|
|
103
|
+
...section,
|
|
104
|
+
accounts,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseAllowFrom(raw: string): string[] {
|
|
111
|
+
return raw
|
|
112
|
+
.split(/[\n,;]+/g)
|
|
113
|
+
.map((entry) => stripChannelPrefix(entry.trim()))
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function promptOptionalAllowFrom(params: {
|
|
118
|
+
cfg: OpenClawConfig;
|
|
119
|
+
accountId: string;
|
|
120
|
+
prompter: {
|
|
121
|
+
confirm: (params: { message: string; initialValue?: boolean }) => Promise<boolean>;
|
|
122
|
+
text: (params: {
|
|
123
|
+
message: string;
|
|
124
|
+
placeholder?: string;
|
|
125
|
+
initialValue?: string;
|
|
126
|
+
validate?: (value: string) => string | undefined;
|
|
127
|
+
}) => Promise<string>;
|
|
128
|
+
};
|
|
129
|
+
}): Promise<OpenClawConfig> {
|
|
130
|
+
const existing = readAccountConfig(params.cfg, params.accountId).allowFrom ?? [];
|
|
131
|
+
const wantsAllowlist = await params.prompter.confirm({
|
|
132
|
+
message: "Set a DM allowlist now? (optional)",
|
|
133
|
+
initialValue: existing.length > 0,
|
|
134
|
+
});
|
|
135
|
+
if (!wantsAllowlist) {
|
|
136
|
+
return params.cfg;
|
|
137
|
+
}
|
|
138
|
+
const raw = await params.prompter.text({
|
|
139
|
+
message: "Allowlist wxid (comma or newline separated)",
|
|
140
|
+
placeholder: "wxid_xxx, wxid_yyy",
|
|
141
|
+
initialValue: existing.map((entry) => String(entry)).join(", "),
|
|
142
|
+
validate: (value) => (parseAllowFrom(value).length > 0 ? undefined : "Required"),
|
|
143
|
+
});
|
|
144
|
+
return setGeweAccountConfig(params.cfg, params.accountId, {
|
|
145
|
+
allowFrom: parseAllowFrom(raw),
|
|
146
|
+
dmPolicy: "allowlist",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function trimTrailingSlash(value: string): string {
|
|
151
|
+
return value.trim().replace(/\/$/, "");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const geweSetupWizard: GeweSetupWizard = {
|
|
155
|
+
channel: CHANNEL_ID,
|
|
156
|
+
status: {
|
|
157
|
+
configuredLabel: "configured",
|
|
158
|
+
unconfiguredLabel: "needs token + appId",
|
|
159
|
+
configuredHint: "configured",
|
|
160
|
+
unconfiguredHint: "needs token + appId",
|
|
161
|
+
configuredScore: 1,
|
|
162
|
+
unconfiguredScore: 0,
|
|
163
|
+
resolveConfigured: ({ cfg }) =>
|
|
164
|
+
listGeweAccountIds(cfg as CoreConfig).some((accountId) => {
|
|
165
|
+
const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
|
|
166
|
+
return Boolean(account.token?.trim() && account.appId?.trim());
|
|
167
|
+
}),
|
|
168
|
+
resolveStatusLines: ({ cfg, configured }) => [
|
|
169
|
+
`GeWe: ${configured ? "configured" : "needs token + appId"}`,
|
|
170
|
+
`Accounts: ${listGeweAccountIds(cfg as CoreConfig).length || 0}`,
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
introNote: {
|
|
174
|
+
title: "GeWe setup",
|
|
175
|
+
lines: [
|
|
176
|
+
"You will need:",
|
|
177
|
+
"- GeWe token + appId",
|
|
178
|
+
"- Public webhook endpoint (FRP or reverse proxy)",
|
|
179
|
+
"- Public media base URL (optional proxy fallback)",
|
|
180
|
+
"Docs: /channels/gewe-openclaw",
|
|
181
|
+
],
|
|
182
|
+
shouldShow: ({ cfg, accountId }) => {
|
|
183
|
+
const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
|
|
184
|
+
return !account.token?.trim() || !account.appId?.trim();
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
credentials: [
|
|
188
|
+
{
|
|
189
|
+
inputKey: "token",
|
|
190
|
+
providerHint: CHANNEL_ID,
|
|
191
|
+
credentialLabel: "token",
|
|
192
|
+
preferredEnvVar: "GEWE_TOKEN",
|
|
193
|
+
envPrompt: "GEWE_TOKEN detected. Use env var?",
|
|
194
|
+
keepPrompt: "GeWe token already configured. Keep it?",
|
|
195
|
+
inputPrompt: "Enter GeWe token",
|
|
196
|
+
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
|
197
|
+
inspect: ({ cfg, accountId }) => {
|
|
198
|
+
const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
|
|
199
|
+
return {
|
|
200
|
+
accountConfigured: Boolean(account.token?.trim() && account.appId?.trim()),
|
|
201
|
+
hasConfiguredValue: Boolean(
|
|
202
|
+
readAccountConfig(cfg, accountId).token?.trim() ||
|
|
203
|
+
readAccountConfig(cfg, accountId).tokenFile?.trim(),
|
|
204
|
+
),
|
|
205
|
+
resolvedValue: account.token?.trim() || undefined,
|
|
206
|
+
envValue:
|
|
207
|
+
accountId === DEFAULT_ACCOUNT_ID
|
|
208
|
+
? process.env.GEWE_TOKEN?.trim() || undefined
|
|
209
|
+
: undefined,
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
applyUseEnv: ({ cfg, accountId }) =>
|
|
213
|
+
clearGeweAccountFields(cfg, accountId, ["token", "tokenFile"]),
|
|
214
|
+
applySet: ({ cfg, accountId, resolvedValue }) =>
|
|
215
|
+
setGeweAccountConfig(clearGeweAccountFields(cfg, accountId, ["tokenFile"]), accountId, {
|
|
216
|
+
token: resolvedValue,
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
inputKey: "appId",
|
|
221
|
+
providerHint: `${CHANNEL_ID}-app`,
|
|
222
|
+
credentialLabel: "appId",
|
|
223
|
+
preferredEnvVar: "GEWE_APP_ID",
|
|
224
|
+
envPrompt: "GEWE_APP_ID detected. Use env var?",
|
|
225
|
+
keepPrompt: "GeWe appId already configured. Keep it?",
|
|
226
|
+
inputPrompt: "Enter GeWe appId",
|
|
227
|
+
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
|
228
|
+
inspect: ({ cfg, accountId }) => {
|
|
229
|
+
const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
|
|
230
|
+
return {
|
|
231
|
+
accountConfigured: Boolean(account.token?.trim() && account.appId?.trim()),
|
|
232
|
+
hasConfiguredValue: Boolean(
|
|
233
|
+
readAccountConfig(cfg, accountId).appId?.trim() ||
|
|
234
|
+
readAccountConfig(cfg, accountId).appIdFile?.trim(),
|
|
235
|
+
),
|
|
236
|
+
resolvedValue: account.appId?.trim() || undefined,
|
|
237
|
+
envValue:
|
|
238
|
+
accountId === DEFAULT_ACCOUNT_ID
|
|
239
|
+
? process.env.GEWE_APP_ID?.trim() || undefined
|
|
240
|
+
: undefined,
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
applyUseEnv: ({ cfg, accountId }) =>
|
|
244
|
+
clearGeweAccountFields(cfg, accountId, ["appId", "appIdFile"]),
|
|
245
|
+
applySet: ({ cfg, accountId, resolvedValue }) =>
|
|
246
|
+
setGeweAccountConfig(clearGeweAccountFields(cfg, accountId, ["appIdFile"]), accountId, {
|
|
247
|
+
appId: resolvedValue,
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
textInputs: [
|
|
252
|
+
{
|
|
253
|
+
inputKey: "apiBaseUrl",
|
|
254
|
+
message: "GeWe API base URL",
|
|
255
|
+
currentValue: ({ cfg, accountId }) => readAccountConfig(cfg, accountId).apiBaseUrl,
|
|
256
|
+
initialValue: ({ cfg, accountId }) =>
|
|
257
|
+
readAccountConfig(cfg, accountId).apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
258
|
+
validate: ({ value }) => (value.trim() ? undefined : "Required"),
|
|
259
|
+
normalizeValue: ({ value }) => trimTrailingSlash(value),
|
|
260
|
+
applySet: ({ cfg, accountId, value }) =>
|
|
261
|
+
setGeweAccountConfig(cfg, accountId, { apiBaseUrl: trimTrailingSlash(value) }),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
inputKey: "httpHost",
|
|
265
|
+
message: "Webhook host",
|
|
266
|
+
currentValue: ({ cfg, accountId }) => readAccountConfig(cfg, accountId).webhookHost,
|
|
267
|
+
initialValue: ({ cfg, accountId }) =>
|
|
268
|
+
readAccountConfig(cfg, accountId).webhookHost ?? DEFAULT_WEBHOOK_HOST,
|
|
269
|
+
validate: ({ value }) => (value.trim() ? undefined : "Required"),
|
|
270
|
+
applySet: ({ cfg, accountId, value }) =>
|
|
271
|
+
setGeweAccountConfig(cfg, accountId, { webhookHost: value.trim() }),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
inputKey: "httpPort",
|
|
275
|
+
message: "Webhook port",
|
|
276
|
+
currentValue: ({ cfg, accountId }) => {
|
|
277
|
+
const port = readAccountConfig(cfg, accountId).webhookPort;
|
|
278
|
+
return typeof port === "number" ? String(port) : undefined;
|
|
279
|
+
},
|
|
280
|
+
initialValue: ({ cfg, accountId }) =>
|
|
281
|
+
String(readAccountConfig(cfg, accountId).webhookPort ?? DEFAULT_WEBHOOK_PORT),
|
|
282
|
+
validate: ({ value }) => {
|
|
283
|
+
const parsed = Number(value);
|
|
284
|
+
return Number.isInteger(parsed) && parsed > 0 ? undefined : "Must be a positive integer";
|
|
285
|
+
},
|
|
286
|
+
applySet: ({ cfg, accountId, value }) =>
|
|
287
|
+
setGeweAccountConfig(cfg, accountId, { webhookPort: Number(value) }),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
inputKey: "webhookPath",
|
|
291
|
+
message: "Webhook path",
|
|
292
|
+
currentValue: ({ cfg, accountId }) => readAccountConfig(cfg, accountId).webhookPath,
|
|
293
|
+
initialValue: ({ cfg, accountId }) =>
|
|
294
|
+
readAccountConfig(cfg, accountId).webhookPath ?? DEFAULT_WEBHOOK_PATH,
|
|
295
|
+
validate: ({ value }) => (value.trim() ? undefined : "Required"),
|
|
296
|
+
normalizeValue: ({ value }) => {
|
|
297
|
+
const trimmed = value.trim();
|
|
298
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
299
|
+
},
|
|
300
|
+
applySet: ({ cfg, accountId, value }) =>
|
|
301
|
+
setGeweAccountConfig(cfg, accountId, {
|
|
302
|
+
webhookPath: value.trim().startsWith("/") ? value.trim() : `/${value.trim()}`,
|
|
303
|
+
}),
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
inputKey: "mediaPublicUrl",
|
|
307
|
+
message: "Media public URL (prefix)",
|
|
308
|
+
placeholder: "https://your-domain/gewe-media",
|
|
309
|
+
required: false,
|
|
310
|
+
applyEmptyValue: true,
|
|
311
|
+
currentValue: ({ cfg, accountId }) => readAccountConfig(cfg, accountId).mediaPublicUrl,
|
|
312
|
+
applySet: ({ cfg, accountId, value }) =>
|
|
313
|
+
setGeweAccountConfig(cfg, accountId, {
|
|
314
|
+
mediaHost: readAccountConfig(cfg, accountId).mediaHost ?? DEFAULT_MEDIA_HOST,
|
|
315
|
+
mediaPort: readAccountConfig(cfg, accountId).mediaPort ?? DEFAULT_MEDIA_PORT,
|
|
316
|
+
mediaPath: readAccountConfig(cfg, accountId).mediaPath ?? DEFAULT_MEDIA_PATH,
|
|
317
|
+
mediaPublicUrl: value.trim() || undefined,
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
allowFrom: {
|
|
322
|
+
message: "Allowlist wxid (comma or newline separated)",
|
|
323
|
+
placeholder: "wxid_xxx, wxid_yyy",
|
|
324
|
+
invalidWithoutCredentialNote: "GeWe allowFrom requires raw wxid values.",
|
|
325
|
+
parseInputs: parseAllowFrom,
|
|
326
|
+
parseId: (raw) => {
|
|
327
|
+
const value = stripChannelPrefix(raw.trim());
|
|
328
|
+
return value || null;
|
|
329
|
+
},
|
|
330
|
+
resolveEntries: async ({ entries }) =>
|
|
331
|
+
entries.map((entry) => {
|
|
332
|
+
const id = stripChannelPrefix(entry.trim());
|
|
333
|
+
return {
|
|
334
|
+
input: entry,
|
|
335
|
+
resolved: Boolean(id),
|
|
336
|
+
id: id || null,
|
|
337
|
+
};
|
|
338
|
+
}),
|
|
339
|
+
apply: ({ cfg, accountId, allowFrom }) =>
|
|
340
|
+
setGeweAccountConfig(cfg, accountId, {
|
|
341
|
+
allowFrom,
|
|
342
|
+
dmPolicy: "allowlist",
|
|
343
|
+
}),
|
|
344
|
+
},
|
|
345
|
+
finalize: async ({ cfg, accountId, prompter, forceAllowFrom }) => {
|
|
346
|
+
let next = cfg;
|
|
347
|
+
const existing = readAccountConfig(next, accountId);
|
|
348
|
+
|
|
349
|
+
const enableS3 = await prompter.confirm({
|
|
350
|
+
message: "Enable S3-compatible media delivery?",
|
|
351
|
+
initialValue: existing.s3Enabled === true,
|
|
352
|
+
});
|
|
353
|
+
if (enableS3) {
|
|
354
|
+
const s3Endpoint = await prompter.text({
|
|
355
|
+
message: "S3 endpoint",
|
|
356
|
+
placeholder: "https://s3.amazonaws.com",
|
|
357
|
+
initialValue: existing.s3Endpoint,
|
|
358
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
359
|
+
});
|
|
360
|
+
const s3Region = await prompter.text({
|
|
361
|
+
message: "S3 region",
|
|
362
|
+
placeholder: "us-east-1",
|
|
363
|
+
initialValue: existing.s3Region,
|
|
364
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
365
|
+
});
|
|
366
|
+
const s3Bucket = await prompter.text({
|
|
367
|
+
message: "S3 bucket",
|
|
368
|
+
initialValue: existing.s3Bucket,
|
|
369
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
370
|
+
});
|
|
371
|
+
const s3AccessKeyId = await prompter.text({
|
|
372
|
+
message: "S3 access key id",
|
|
373
|
+
initialValue: existing.s3AccessKeyId,
|
|
374
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
375
|
+
});
|
|
376
|
+
const s3SecretAccessKey = await prompter.text({
|
|
377
|
+
message: "S3 secret access key",
|
|
378
|
+
initialValue: existing.s3SecretAccessKey,
|
|
379
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
380
|
+
});
|
|
381
|
+
const s3SessionToken = await prompter.text({
|
|
382
|
+
message: "S3 session token (optional)",
|
|
383
|
+
initialValue: existing.s3SessionToken,
|
|
384
|
+
});
|
|
385
|
+
const s3ForcePathStyle = await prompter.confirm({
|
|
386
|
+
message: "Use path-style for S3 endpoint?",
|
|
387
|
+
initialValue: existing.s3ForcePathStyle === true,
|
|
388
|
+
});
|
|
389
|
+
const s3KeyPrefix = await prompter.text({
|
|
390
|
+
message: "S3 key prefix (optional)",
|
|
391
|
+
placeholder: "gewe-openclaw/outbound",
|
|
392
|
+
initialValue: existing.s3KeyPrefix,
|
|
393
|
+
});
|
|
394
|
+
const s3UrlMode = (await prompter.select({
|
|
395
|
+
message: "S3 URL mode",
|
|
396
|
+
options: [
|
|
397
|
+
{ value: "public", label: "public (default)" },
|
|
398
|
+
{ value: "presigned", label: "presigned" },
|
|
399
|
+
],
|
|
400
|
+
initialValue: existing.s3UrlMode ?? "public",
|
|
401
|
+
})) as NonNullable<GeweAccountConfig["s3UrlMode"]>;
|
|
402
|
+
const s3PublicBaseUrl =
|
|
403
|
+
s3UrlMode === "public"
|
|
404
|
+
? await prompter.text({
|
|
405
|
+
message: "S3 public base URL",
|
|
406
|
+
placeholder: "https://cdn.example.com/gewe-media",
|
|
407
|
+
initialValue: existing.s3PublicBaseUrl,
|
|
408
|
+
validate: (value) => (value.trim() ? undefined : "Required"),
|
|
409
|
+
})
|
|
410
|
+
: await prompter.text({
|
|
411
|
+
message: "S3 public base URL (optional in presigned mode)",
|
|
412
|
+
initialValue: existing.s3PublicBaseUrl,
|
|
413
|
+
});
|
|
414
|
+
const s3PresignExpiresSecRaw =
|
|
415
|
+
s3UrlMode === "presigned"
|
|
416
|
+
? await prompter.text({
|
|
417
|
+
message: "Presigned URL expire seconds",
|
|
418
|
+
initialValue: String(existing.s3PresignExpiresSec ?? 3600),
|
|
419
|
+
validate: (value) => {
|
|
420
|
+
const parsed = Number(value);
|
|
421
|
+
return Number.isInteger(parsed) && parsed > 0
|
|
422
|
+
? undefined
|
|
423
|
+
: "Must be a positive integer";
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
: "";
|
|
427
|
+
|
|
428
|
+
next = setGeweAccountConfig(next, accountId, {
|
|
429
|
+
s3Enabled: true,
|
|
430
|
+
s3Endpoint: s3Endpoint.trim(),
|
|
431
|
+
s3Region: s3Region.trim(),
|
|
432
|
+
s3Bucket: s3Bucket.trim(),
|
|
433
|
+
s3AccessKeyId: s3AccessKeyId.trim(),
|
|
434
|
+
s3SecretAccessKey: s3SecretAccessKey.trim(),
|
|
435
|
+
s3SessionToken: s3SessionToken.trim() || undefined,
|
|
436
|
+
s3ForcePathStyle,
|
|
437
|
+
s3KeyPrefix: s3KeyPrefix.trim() || undefined,
|
|
438
|
+
s3UrlMode,
|
|
439
|
+
s3PublicBaseUrl: s3PublicBaseUrl.trim() || undefined,
|
|
440
|
+
s3PresignExpiresSec:
|
|
441
|
+
s3UrlMode === "presigned" ? Number(s3PresignExpiresSecRaw) : undefined,
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
next = setGeweAccountConfig(next, accountId, { s3Enabled: false });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!forceAllowFrom) {
|
|
448
|
+
next = await promptOptionalAllowFrom({
|
|
449
|
+
cfg: next,
|
|
450
|
+
accountId,
|
|
451
|
+
prompter,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return { cfg: next };
|
|
456
|
+
},
|
|
457
|
+
completionNote: {
|
|
458
|
+
title: "GeWe restart required",
|
|
459
|
+
lines: [
|
|
460
|
+
"Restart the OpenClaw gateway after saving GeWe config.",
|
|
461
|
+
"Make sure your webhook endpoint and optional mediaPublicUrl are publicly reachable.",
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
};
|
package/src/silk.ts
CHANGED
|
@@ -5,6 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import type { ReadableStream as WebReadableStream } from "node:stream/web";
|
|
8
9
|
|
|
9
10
|
import type { ResolvedGeweAccount } from "./types.js";
|
|
10
11
|
import { getGeweRuntime } from "./runtime.js";
|
|
@@ -352,7 +353,7 @@ async function downloadFile(url: string, dest: string): Promise<void> {
|
|
|
352
353
|
if (!response.body) {
|
|
353
354
|
throw new Error("download failed: empty response body");
|
|
354
355
|
}
|
|
355
|
-
const stream = Readable.fromWeb(response.body as unknown as
|
|
356
|
+
const stream = Readable.fromWeb(response.body as unknown as WebReadableStream);
|
|
356
357
|
await pipeline(stream, createWriteStream(dest));
|
|
357
358
|
}
|
|
358
359
|
|
package/src/state-paths.ts
CHANGED
|
@@ -2,11 +2,62 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
function normalize(input: string | undefined): string | undefined {
|
|
6
|
+
const trimmed = input?.trim();
|
|
7
|
+
return trimmed ? trimmed : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveEffectiveHomeDir(
|
|
11
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
12
|
+
homedir: () => string = os.homedir,
|
|
13
|
+
): string | undefined {
|
|
14
|
+
const explicitHome = normalize(env.OPENCLAW_HOME);
|
|
15
|
+
if (explicitHome) {
|
|
16
|
+
if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) {
|
|
17
|
+
const fallbackHome =
|
|
18
|
+
normalize(env.HOME) ?? normalize(env.USERPROFILE) ?? normalizeSafe(homedir);
|
|
19
|
+
if (fallbackHome) {
|
|
20
|
+
return path.resolve(explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome));
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return path.resolve(explicitHome);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const envHome = normalize(env.HOME);
|
|
28
|
+
if (envHome) return path.resolve(envHome);
|
|
29
|
+
|
|
30
|
+
const userProfile = normalize(env.USERPROFILE);
|
|
31
|
+
if (userProfile) return path.resolve(userProfile);
|
|
32
|
+
|
|
33
|
+
const home = normalizeSafe(homedir);
|
|
34
|
+
return home ? path.resolve(home) : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeSafe(homedir: () => string): string | undefined {
|
|
38
|
+
try {
|
|
39
|
+
return normalize(homedir());
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveRequiredHomeDir(
|
|
46
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
47
|
+
homedir: () => string = os.homedir,
|
|
48
|
+
): string {
|
|
49
|
+
return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveUserPath(
|
|
53
|
+
input: string,
|
|
54
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
55
|
+
homedir: () => string = os.homedir,
|
|
56
|
+
): string {
|
|
6
57
|
const trimmed = input.trim();
|
|
7
58
|
if (!trimmed) return trimmed;
|
|
8
59
|
if (trimmed.startsWith("~")) {
|
|
9
|
-
const expanded = trimmed.replace(/^~(?=$|[\\/])/, homedir
|
|
60
|
+
const expanded = trimmed.replace(/^~(?=$|[\\/])/, resolveRequiredHomeDir(env, homedir));
|
|
10
61
|
return path.resolve(expanded);
|
|
11
62
|
}
|
|
12
63
|
return path.resolve(trimmed);
|
|
@@ -18,22 +69,12 @@ export function resolveOpenClawStateDir(
|
|
|
18
69
|
exists: (target: string) => boolean = existsSync,
|
|
19
70
|
): string {
|
|
20
71
|
const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
|
|
21
|
-
if (override) return resolveUserPath(override, homedir);
|
|
72
|
+
if (override) return resolveUserPath(override, env, homedir);
|
|
22
73
|
|
|
23
|
-
const
|
|
24
|
-
const newDir = path.join(home, ".openclaw");
|
|
25
|
-
const legacyDirs = [".clawdbot", ".moltbot", ".moldbot"].map((dir) => path.join(home, dir));
|
|
74
|
+
const newDir = path.join(resolveRequiredHomeDir(env, homedir), ".openclaw");
|
|
26
75
|
|
|
27
76
|
try {
|
|
28
77
|
if (exists(newDir)) return newDir;
|
|
29
|
-
const existingLegacy = legacyDirs.find((candidate) => {
|
|
30
|
-
try {
|
|
31
|
-
return exists(candidate);
|
|
32
|
-
} catch {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
if (existingLegacy) return existingLegacy;
|
|
37
78
|
} catch {
|
|
38
79
|
// best-effort
|
|
39
80
|
}
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,52 @@ import type {
|
|
|
4
4
|
DmPolicy,
|
|
5
5
|
GroupPolicy,
|
|
6
6
|
GroupToolPolicyConfig,
|
|
7
|
-
|
|
7
|
+
OpenClawConfig,
|
|
8
|
+
} from "./openclaw-compat.js";
|
|
9
|
+
|
|
10
|
+
export type GeweGroupTriggerMode = "at" | "quote" | "at_or_quote" | "any_message";
|
|
11
|
+
export type GeweDmTriggerMode = "any_message" | "quote";
|
|
12
|
+
export type GeweGroupReplyMode = "plain" | "quote_source" | "at_sender" | "quote_and_at";
|
|
13
|
+
export type GeweDmReplyMode = "plain" | "quote_source";
|
|
14
|
+
|
|
15
|
+
export type GeweGroupTriggerConfig = {
|
|
16
|
+
mode?: GeweGroupTriggerMode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type GeweDmTriggerConfig = {
|
|
20
|
+
mode?: GeweDmTriggerMode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type GeweGroupReplyConfig = {
|
|
24
|
+
mode?: GeweGroupReplyMode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type GeweDmReplyConfig = {
|
|
28
|
+
mode?: GeweDmReplyMode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type GeweBindingIdentitySelfSource = "agent_name" | "agent_id" | "literal";
|
|
32
|
+
export type GeweBindingIdentityRemarkSource =
|
|
33
|
+
| "agent_id"
|
|
34
|
+
| "agent_name"
|
|
35
|
+
| "name_and_id"
|
|
36
|
+
| "literal";
|
|
37
|
+
|
|
38
|
+
export type GeweBindingIdentitySelfConfig = {
|
|
39
|
+
source?: GeweBindingIdentitySelfSource;
|
|
40
|
+
value?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type GeweBindingIdentityRemarkConfig = {
|
|
44
|
+
source?: GeweBindingIdentityRemarkSource;
|
|
45
|
+
value?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type GeweGroupBindingIdentityConfig = {
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
selfNickname?: GeweBindingIdentitySelfConfig;
|
|
51
|
+
remark?: GeweBindingIdentityRemarkConfig;
|
|
52
|
+
};
|
|
8
53
|
|
|
9
54
|
export type GeweGroupConfig = {
|
|
10
55
|
requireMention?: boolean;
|
|
@@ -13,6 +58,16 @@ export type GeweGroupConfig = {
|
|
|
13
58
|
enabled?: boolean;
|
|
14
59
|
allowFrom?: string[];
|
|
15
60
|
systemPrompt?: string;
|
|
61
|
+
trigger?: GeweGroupTriggerConfig;
|
|
62
|
+
reply?: GeweGroupReplyConfig;
|
|
63
|
+
bindingIdentity?: GeweGroupBindingIdentityConfig;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type GeweDmConfig = DmConfig & {
|
|
67
|
+
skills?: string[];
|
|
68
|
+
systemPrompt?: string;
|
|
69
|
+
trigger?: GeweDmTriggerConfig;
|
|
70
|
+
reply?: GeweDmReplyConfig;
|
|
16
71
|
};
|
|
17
72
|
|
|
18
73
|
export type GeweAccountConfig = {
|
|
@@ -73,9 +128,10 @@ export type GeweAccountConfig = {
|
|
|
73
128
|
groups?: Record<string, GeweGroupConfig>;
|
|
74
129
|
historyLimit?: number;
|
|
75
130
|
dmHistoryLimit?: number;
|
|
76
|
-
dms?: Record<string,
|
|
131
|
+
dms?: Record<string, GeweDmConfig>;
|
|
77
132
|
textChunkLimit?: number;
|
|
78
133
|
chunkMode?: "length" | "newline";
|
|
134
|
+
autoQuoteReply?: boolean;
|
|
79
135
|
blockStreaming?: boolean;
|
|
80
136
|
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
|
81
137
|
};
|
|
@@ -84,11 +140,13 @@ export type GeweConfig = {
|
|
|
84
140
|
accounts?: Record<string, GeweAccountConfig>;
|
|
85
141
|
} & GeweAccountConfig;
|
|
86
142
|
|
|
87
|
-
export type CoreConfig = {
|
|
88
|
-
channels?: {
|
|
143
|
+
export type CoreConfig = OpenClawConfig & {
|
|
144
|
+
channels?: OpenClawConfig["channels"] & {
|
|
145
|
+
defaults?: {
|
|
146
|
+
groupPolicy?: GroupPolicy;
|
|
147
|
+
};
|
|
89
148
|
"gewe-openclaw"?: GeweConfig;
|
|
90
149
|
};
|
|
91
|
-
[key: string]: unknown;
|
|
92
150
|
};
|
|
93
151
|
|
|
94
152
|
export type GeweTokenSource = "env" | "config" | "configFile" | "none";
|
|
@@ -111,8 +169,8 @@ export type GeweCallbackPayload = {
|
|
|
111
169
|
Appid?: string;
|
|
112
170
|
Wxid?: string;
|
|
113
171
|
Data?: {
|
|
114
|
-
MsgId?: number;
|
|
115
|
-
NewMsgId?: number;
|
|
172
|
+
MsgId?: number | string;
|
|
173
|
+
NewMsgId?: number | string;
|
|
116
174
|
FromUserName?: { string?: string };
|
|
117
175
|
ToUserName?: { string?: string };
|
|
118
176
|
MsgType?: number;
|
|
@@ -142,6 +200,7 @@ export type GeweWebhookServerOptions = {
|
|
|
142
200
|
port: number;
|
|
143
201
|
host: string;
|
|
144
202
|
path: string;
|
|
203
|
+
mediaPath?: string;
|
|
145
204
|
secret?: string;
|
|
146
205
|
onMessage: (message: GeweInboundMessage) => void | Promise<void>;
|
|
147
206
|
onError?: (error: Error) => void;
|