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.
Files changed (44) hide show
  1. package/README.md +455 -3
  2. package/index.ts +39 -1
  3. package/package.json +12 -1
  4. package/skills/gewe-agent-tools/SKILL.md +113 -0
  5. package/skills/gewe-channel-rules/SKILL.md +7 -0
  6. package/src/accounts.ts +51 -5
  7. package/src/api-tools.ts +1264 -0
  8. package/src/api.ts +37 -2
  9. package/src/binary-command.ts +65 -0
  10. package/src/channel-actions.ts +536 -0
  11. package/src/channel-allowlist.ts +150 -0
  12. package/src/channel-directory.ts +419 -0
  13. package/src/channel-status.ts +186 -0
  14. package/src/channel.ts +155 -58
  15. package/src/config-edit.ts +94 -0
  16. package/src/config-schema.ts +78 -3
  17. package/src/contacts-api.ts +113 -0
  18. package/src/delivery.ts +502 -62
  19. package/src/directory-cache.ts +164 -0
  20. package/src/gewe-account-api.ts +27 -0
  21. package/src/group-allowlist-tool.ts +242 -0
  22. package/src/group-binding-tool.ts +154 -0
  23. package/src/group-binding.ts +405 -0
  24. package/src/groups-api.ts +146 -0
  25. package/src/inbound-batch.ts +5 -2
  26. package/src/inbound.ts +248 -41
  27. package/src/media-server.ts +73 -93
  28. package/src/moments-api.ts +138 -0
  29. package/src/monitor.ts +81 -24
  30. package/src/onboarding.ts +9 -4
  31. package/src/openclaw-compat.ts +1070 -0
  32. package/src/pairing-store.ts +478 -0
  33. package/src/personal-api.ts +45 -0
  34. package/src/policy.ts +130 -22
  35. package/src/quote-context-cache.ts +97 -0
  36. package/src/reply-options.ts +101 -2
  37. package/src/s3.ts +1 -1
  38. package/src/send.ts +235 -16
  39. package/src/setup-wizard-types.ts +162 -0
  40. package/src/setup-wizard.ts +464 -0
  41. package/src/silk.ts +2 -1
  42. package/src/state-paths.ts +55 -14
  43. package/src/types.ts +66 -7
  44. 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 ReadableStream);
356
+ const stream = Readable.fromWeb(response.body as unknown as WebReadableStream);
356
357
  await pipeline(stream, createWriteStream(dest));
357
358
  }
358
359
 
@@ -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
- export function resolveUserPath(input: string, homedir: () => string = os.homedir): string {
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 home = homedir();
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
- } from "openclaw/plugin-sdk";
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, DmConfig>;
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;