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,186 @@
1
+ import type { ChannelStatusAdapter, ChannelStatusIssue } from "openclaw/plugin-sdk/channel-runtime";
2
+
3
+ import type { ResolvedGeweAccount } from "./accounts.js";
4
+ import { collectKnownGeweGroupEntries, collectKnownGewePeerEntries } from "./channel-directory.js";
5
+ import { getGeweDirectoryCacheCounts } from "./directory-cache.js";
6
+ import { CHANNEL_CONFIG_KEY } from "./constants.js";
7
+ import { getGeweProfile } from "./group-binding.js";
8
+ import { normalizeGeweBindingConversationId } from "./group-binding.js";
9
+ import { normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
10
+ import { readGeweAllowFromStore } from "./pairing-store.js";
11
+
12
+ type GeweStatusProbe = {
13
+ ok: boolean;
14
+ latencyMs?: number;
15
+ self?: {
16
+ wxid: string;
17
+ nickName?: string;
18
+ };
19
+ error?: string;
20
+ };
21
+
22
+ type BindingLike = {
23
+ match?: {
24
+ channel?: string;
25
+ accountId?: string;
26
+ peer?: {
27
+ kind?: string;
28
+ id?: string;
29
+ };
30
+ };
31
+ };
32
+
33
+ const CHANNEL_ALIASES = new Set(["gewe-openclaw", "gewe", "wechat", "wx"]);
34
+
35
+ function listConfigBindings(cfg: OpenClawConfig): BindingLike[] {
36
+ return Array.isArray(cfg.bindings) ? (cfg.bindings as BindingLike[]) : [];
37
+ }
38
+
39
+ function normalizeBindingChannel(value?: string): string {
40
+ const trimmed = value?.trim().toLowerCase() ?? "";
41
+ return CHANNEL_ALIASES.has(trimmed) ? CHANNEL_CONFIG_KEY : trimmed;
42
+ }
43
+
44
+ function bindingMatchesAccount(bindingAccountId: string | undefined, accountId: string): boolean {
45
+ const trimmed = bindingAccountId?.trim();
46
+ if (!trimmed) {
47
+ return accountId === "default";
48
+ }
49
+ if (trimmed === "*") {
50
+ return true;
51
+ }
52
+ return normalizeAccountId(trimmed) === accountId;
53
+ }
54
+
55
+ function countExplicitBindings(cfg: OpenClawConfig, accountId: string): number {
56
+ let count = 0;
57
+ for (const binding of listConfigBindings(cfg)) {
58
+ if (normalizeBindingChannel(binding.match?.channel) !== CHANNEL_CONFIG_KEY) {
59
+ continue;
60
+ }
61
+ if (!bindingMatchesAccount(binding.match?.accountId, accountId)) {
62
+ continue;
63
+ }
64
+ const normalizedId = normalizeGeweBindingConversationId(binding.match?.peer?.id);
65
+ if (!normalizedId) {
66
+ continue;
67
+ }
68
+ count += 1;
69
+ }
70
+ return count;
71
+ }
72
+
73
+ function countGroupOverrides(account: ResolvedGeweAccount): number {
74
+ return Object.entries(account.config.groups ?? {}).filter(
75
+ ([, group]) => Array.isArray(group?.allowFrom) && group.allowFrom.length > 0,
76
+ ).length;
77
+ }
78
+
79
+ export const geweStatus: ChannelStatusAdapter<ResolvedGeweAccount, GeweStatusProbe> = {
80
+ probeAccount: async ({ account }) => {
81
+ const startedAt = Date.now();
82
+ try {
83
+ const profile = await getGeweProfile({ account });
84
+ return {
85
+ ok: true,
86
+ latencyMs: Date.now() - startedAt,
87
+ self: {
88
+ wxid: profile.wxid,
89
+ nickName: profile.nickName,
90
+ },
91
+ };
92
+ } catch (err) {
93
+ return {
94
+ ok: false,
95
+ latencyMs: Date.now() - startedAt,
96
+ error: String(err),
97
+ };
98
+ }
99
+ },
100
+ buildChannelSummary: async ({ snapshot }) => ({
101
+ configured: snapshot.configured ?? false,
102
+ tokenSource: snapshot.tokenSource ?? "none",
103
+ running: snapshot.running ?? false,
104
+ mode: snapshot.mode ?? null,
105
+ lastStartAt: snapshot.lastStartAt ?? null,
106
+ lastStopAt: snapshot.lastStopAt ?? null,
107
+ lastError: snapshot.lastError ?? null,
108
+ lastInboundAt: snapshot.lastInboundAt ?? null,
109
+ lastOutboundAt: snapshot.lastOutboundAt ?? null,
110
+ apiReachable: snapshot.apiReachable ?? false,
111
+ apiLatencyMs: snapshot.apiLatencyMs ?? null,
112
+ self: snapshot.self ?? null,
113
+ knownPeersCount: snapshot.knownPeersCount ?? 0,
114
+ knownGroupsCount: snapshot.knownGroupsCount ?? 0,
115
+ cachedGroupMemberCount: snapshot.cachedGroupMemberCount ?? 0,
116
+ explicitBindingCount: snapshot.explicitBindingCount ?? 0,
117
+ groupOverrideCount: snapshot.groupOverrideCount ?? 0,
118
+ pairingAllowFromCount: snapshot.pairingAllowFromCount ?? 0,
119
+ }),
120
+ buildAccountSnapshot: async ({ account, runtime, cfg, probe }) => {
121
+ const configured = Boolean(account.token?.trim() && account.appId?.trim());
122
+ const pairingEntries = await readGeweAllowFromStore({
123
+ accountId: account.accountId,
124
+ }).catch(() => []);
125
+ const peerEntries = collectKnownGewePeerEntries({
126
+ cfg,
127
+ accountId: account.accountId,
128
+ });
129
+ const groupEntries = collectKnownGeweGroupEntries({
130
+ cfg,
131
+ accountId: account.accountId,
132
+ });
133
+ const cacheCounts = getGeweDirectoryCacheCounts(account.accountId);
134
+ return {
135
+ accountId: account.accountId,
136
+ name: account.name,
137
+ enabled: account.enabled,
138
+ configured,
139
+ tokenSource: account.tokenSource,
140
+ baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
141
+ running: runtime?.running ?? false,
142
+ lastStartAt: runtime?.lastStartAt ?? null,
143
+ lastStopAt: runtime?.lastStopAt ?? null,
144
+ lastError: runtime?.lastError ?? null,
145
+ mode: "webhook",
146
+ lastInboundAt: runtime?.lastInboundAt ?? null,
147
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
148
+ dmPolicy: account.config.dmPolicy ?? "pairing",
149
+ groupPolicy: account.config.groupPolicy ?? "allowlist",
150
+ apiReachable: probe?.ok ?? false,
151
+ apiLatencyMs: probe?.latencyMs ?? null,
152
+ self: probe?.self ?? null,
153
+ knownPeersCount: peerEntries.length,
154
+ knownGroupsCount: groupEntries.length,
155
+ cachedGroupMemberCount: cacheCounts.cachedGroupMemberCount,
156
+ explicitBindingCount: countExplicitBindings(cfg, account.accountId),
157
+ groupOverrideCount: countGroupOverrides(account),
158
+ pairingAllowFromCount: pairingEntries.length,
159
+ };
160
+ },
161
+ collectStatusIssues: (accounts) => {
162
+ const issues: ChannelStatusIssue[] = [];
163
+ for (const account of accounts) {
164
+ if (account.configured && account.apiReachable === false) {
165
+ issues.push({
166
+ channel: CHANNEL_CONFIG_KEY,
167
+ accountId: account.accountId,
168
+ kind: "runtime",
169
+ message: `GeWe API probe failed for account "${account.accountId}".`,
170
+ fix: "Check token/appId, API base URL, and GeWe service availability.",
171
+ });
172
+ }
173
+ if (account.groupPolicy === "open" && (account.groupOverrideCount ?? 0) === 0) {
174
+ issues.push({
175
+ channel: CHANNEL_CONFIG_KEY,
176
+ accountId: account.accountId,
177
+ kind: "config",
178
+ message:
179
+ 'GeWe groupPolicy="open" is active without any per-group allowFrom override.',
180
+ fix: `Set channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom or groups.<room>.allowFrom to narrow group access.`,
181
+ });
182
+ }
183
+ }
184
+ return issues;
185
+ },
186
+ };
package/src/channel.ts CHANGED
@@ -3,17 +3,20 @@ import {
3
3
  buildChannelConfigSchema,
4
4
  DEFAULT_ACCOUNT_ID,
5
5
  deleteAccountFromConfigSection,
6
- formatPairingApproveHint,
7
6
  missingTargetError,
8
7
  normalizeAccountId,
9
8
  PAIRING_APPROVED_MESSAGE,
10
9
  setAccountEnabledInConfigSection,
11
- type ChannelPlugin,
12
10
  type OpenClawConfig,
13
11
  type ChannelSetupInput,
14
- } from "openclaw/plugin-sdk";
12
+ type ReplyPayload,
13
+ } from "./openclaw-compat.js";
15
14
 
16
15
  import { resolveGeweAccount, resolveDefaultGeweAccountId, listGeweAccountIds } from "./accounts.js";
16
+ import { geweMessageActions } from "./channel-actions.js";
17
+ import { geweAllowlist } from "./channel-allowlist.js";
18
+ import { geweDirectory } from "./channel-directory.js";
19
+ import { geweStatus } from "./channel-status.js";
17
20
  import { GeweConfigSchema } from "./config-schema.js";
18
21
  import {
19
22
  CHANNEL_ALIASES,
@@ -29,8 +32,10 @@ import { looksLikeGeweTargetId, normalizeGeweMessagingTarget } from "./normalize
29
32
  import { resolveGeweGroupToolPolicy, resolveGeweRequireMention } from "./policy.js";
30
33
  import { getGeweRuntime } from "./runtime.js";
31
34
  import { sendTextGewe } from "./send.js";
35
+ import { geweSetupWizard } from "./setup-wizard.js";
36
+ import type { GeweChannelPlugin } from "./setup-wizard-types.js";
37
+ import { normalizeGeweBindingConversationId } from "./group-binding.js";
32
38
  import type { CoreConfig, ResolvedGeweAccount } from "./types.js";
33
- import { geweOnboarding } from "./onboarding.js";
34
39
 
35
40
  const meta = {
36
41
  id: CHANNEL_ID,
@@ -53,25 +58,119 @@ type GeweSetupInput = ChannelSetupInput & {
53
58
  apiBaseUrl?: string;
54
59
  };
55
60
 
56
- export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
57
- id: CHANNEL_ID,
58
- meta,
59
- onboarding: geweOnboarding,
60
- pairing: {
61
- idLabel: "wechatUserId",
62
- normalizeAllowEntry: (entry) => stripChannelPrefix(entry),
63
- notifyApproval: async ({ cfg, id }) => {
64
- const account = resolveGeweAccount({ cfg: cfg as CoreConfig });
65
- if (!account.token || !account.appId) {
66
- throw new Error("GeWe token/appId not configured");
67
- }
68
- await sendTextGewe({
69
- account,
70
- toWxid: id,
71
- content: PAIRING_APPROVED_MESSAGE,
72
- });
61
+ const GEWE_QUOTE_PARTIAL_DIRECTIVE_RE = /(?:\r?\n)?\s*\[\[GEWE_QUOTE_PARTIAL:([\s\S]*?)\]\]\s*$/;
62
+
63
+ function asRecord(value: unknown): Record<string, unknown> | null {
64
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
65
+ return null;
66
+ }
67
+ return value as Record<string, unknown>;
68
+ }
69
+
70
+ function withGeweScopedChannelData(
71
+ payload: ReplyPayload,
72
+ updater: (scoped: Record<string, unknown>) => Record<string, unknown>,
73
+ ): ReplyPayload {
74
+ const channelData = asRecord(payload.channelData) ?? {};
75
+ const existingChannelScoped = asRecord(channelData[CHANNEL_ID]);
76
+ const existingLegacyScoped = asRecord(channelData.gewe);
77
+ const targetKey = existingChannelScoped ? CHANNEL_ID : existingLegacyScoped ? "gewe" : CHANNEL_ID;
78
+ const scoped = existingChannelScoped ?? existingLegacyScoped ?? {};
79
+
80
+ return {
81
+ ...payload,
82
+ channelData: {
83
+ ...channelData,
84
+ [targetKey]: updater(scoped),
73
85
  },
86
+ };
87
+ }
88
+
89
+ function parseGeweQuotePartialDirective(text: string | undefined): {
90
+ cleanedText: string;
91
+ partialText: string;
92
+ } | null {
93
+ if (typeof text !== "string") return null;
94
+ const match = GEWE_QUOTE_PARTIAL_DIRECTIVE_RE.exec(text);
95
+ if (!match) return null;
96
+ const partialText = match[1]?.trim();
97
+ if (!partialText) return null;
98
+ const cleanedText = text.slice(0, match.index).replace(/\s+$/, "");
99
+ return {
100
+ cleanedText,
101
+ partialText,
102
+ };
103
+ }
104
+
105
+ function resolvePayloadMediaEntries(payload: ReplyPayload): string[] {
106
+ const mediaUrls = payload.mediaUrls?.map((entry) => entry?.trim()).filter(Boolean);
107
+ if (mediaUrls?.length) {
108
+ return mediaUrls;
109
+ }
110
+ const mediaUrl = payload.mediaUrl?.trim();
111
+ return mediaUrl ? [mediaUrl] : [];
112
+ }
113
+
114
+ function normalizeGeweOutboundPayload(payload: ReplyPayload): ReplyPayload {
115
+ let normalized = payload;
116
+
117
+ const partialDirective = parseGeweQuotePartialDirective(payload.text);
118
+ if (partialDirective) {
119
+ normalized = {
120
+ ...normalized,
121
+ text: partialDirective.cleanedText,
122
+ };
123
+ normalized = withGeweScopedChannelData(normalized, (scoped) => {
124
+ const quoteReply = asRecord(scoped.quoteReply);
125
+ const existingPartialText = asRecord(quoteReply?.partialText);
126
+ if (existingPartialText?.text) {
127
+ return scoped;
128
+ }
129
+ return {
130
+ ...scoped,
131
+ quoteReply: {
132
+ ...(quoteReply ?? {}),
133
+ partialText: {
134
+ ...existingPartialText,
135
+ text: partialDirective.partialText,
136
+ },
137
+ },
138
+ };
139
+ });
140
+ }
141
+
142
+ if (normalized.audioAsVoice !== true || resolvePayloadMediaEntries(normalized).length !== 1) {
143
+ return normalized;
144
+ }
145
+
146
+ return withGeweScopedChannelData(normalized, (scoped) => ({
147
+ ...scoped,
148
+ audioAsVoice: true,
149
+ }));
150
+ }
151
+
152
+ const gewePairing = {
153
+ idLabel: "wechatUserId",
154
+ mode: "code" as const,
155
+ normalizeAllowEntry: (entry: string) => stripChannelPrefix(entry),
156
+ notifyApproval: async ({ cfg, id }: { cfg: OpenClawConfig; id: string }) => {
157
+ const account = resolveGeweAccount({ cfg: cfg as CoreConfig });
158
+ if (!account.token || !account.appId) {
159
+ throw new Error("GeWe token/appId not configured");
160
+ }
161
+ await sendTextGewe({
162
+ account,
163
+ toWxid: id,
164
+ content: PAIRING_APPROVED_MESSAGE,
165
+ });
74
166
  },
167
+ };
168
+
169
+ export const gewePlugin: GeweChannelPlugin<ResolvedGeweAccount> = {
170
+ id: CHANNEL_ID,
171
+ meta,
172
+ setupWizard: geweSetupWizard,
173
+ pairing: gewePairing as GeweChannelPlugin<ResolvedGeweAccount>["pairing"],
75
174
  capabilities: {
76
175
  chatTypes: ["direct", "group"],
77
176
  reactions: false,
@@ -120,6 +219,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
120
219
  .filter(Boolean)
121
220
  .map((entry) => stripChannelPrefix(entry)),
122
221
  },
222
+ allowlist: geweAllowlist,
123
223
  security: {
124
224
  resolveDmPolicy: ({ cfg, accountId, account }) => {
125
225
  const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
@@ -134,7 +234,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
134
234
  allowFrom: account.config.allowFrom ?? [],
135
235
  policyPath: `${basePath}dmPolicy`,
136
236
  allowFromPath: basePath,
137
- approveHint: formatPairingApproveHint(CHANNEL_ID),
237
+ approveHint: "Issue a pair code with: openclaw pairing code create gewe-openclaw",
138
238
  normalizeEntry: (raw) => stripChannelPrefix(raw),
139
239
  };
140
240
  },
@@ -146,14 +246,34 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
146
246
  account.config.groups && Object.keys(account.config.groups).length > 0;
147
247
  if (groupAllowlistConfigured) {
148
248
  return [
149
- `- GeWe groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom to restrict senders.`,
249
+ `- GeWe groups: groupPolicy="open" allows any member in allowed groups to trigger (at-gated by default). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom to restrict senders.`,
150
250
  ];
151
251
  }
152
252
  return [
153
- `- GeWe groups: groupPolicy="open" with no channels.${CHANNEL_CONFIG_KEY}.groups allowlist; any group can add + ping (mention-gated). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom or configure channels.${CHANNEL_CONFIG_KEY}.groups.`,
253
+ `- GeWe groups: groupPolicy="open" with no channels.${CHANNEL_CONFIG_KEY}.groups allowlist; any group can add + at (at-gated by default). Set channels.${CHANNEL_CONFIG_KEY}.groupPolicy="allowlist" + channels.${CHANNEL_CONFIG_KEY}.groupAllowFrom or configure channels.${CHANNEL_CONFIG_KEY}.groups.`,
154
254
  ];
155
255
  },
156
256
  },
257
+ bindings: {
258
+ compileConfiguredBinding: ({ conversationId }) => {
259
+ const normalized = normalizeGeweBindingConversationId(conversationId);
260
+ return normalized
261
+ ? {
262
+ conversationId: normalized,
263
+ }
264
+ : null;
265
+ },
266
+ matchInboundConversation: ({ compiledBinding, conversationId }) => {
267
+ const normalized = normalizeGeweBindingConversationId(conversationId);
268
+ if (!normalized || normalized !== compiledBinding.conversationId) {
269
+ return null;
270
+ }
271
+ return {
272
+ conversationId: normalized,
273
+ matchPriority: 2,
274
+ };
275
+ },
276
+ },
157
277
  groups: {
158
278
  resolveRequireMention: ({ cfg, groupId, accountId }) => {
159
279
  const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
@@ -168,19 +288,23 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
168
288
  resolveToolPolicy: resolveGeweGroupToolPolicy,
169
289
  },
170
290
  messaging: {
171
- normalizeTarget: normalizeGeweMessagingTarget,
291
+ normalizeTarget: (raw) => normalizeGeweMessagingTarget(raw) ?? undefined,
172
292
  targetResolver: {
173
293
  looksLikeId: looksLikeGeweTargetId,
174
294
  hint: "<wxid|@chatroom>",
175
295
  },
176
296
  },
297
+ directory: geweDirectory,
298
+ actions: geweMessageActions,
177
299
  outbound: {
178
300
  deliveryMode: "direct",
301
+ normalizePayload: ({ payload }) => normalizeGeweOutboundPayload(payload),
179
302
  chunker: (text, limit) => {
180
303
  const core = getGeweRuntime();
181
304
  return core.channel.text.chunkMarkdownText(text, limit);
182
305
  },
183
306
  chunkerMode: "markdown",
307
+ textChunkLimit: 4000,
184
308
  resolveTarget: ({ to, allowFrom, mode }) => {
185
309
  const trimmed = to?.trim() ?? "";
186
310
  const allowListRaw = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
@@ -271,36 +395,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
271
395
  lastStopAt: null,
272
396
  lastError: null,
273
397
  },
274
- buildChannelSummary: ({ snapshot }) => ({
275
- configured: snapshot.configured ?? false,
276
- tokenSource: snapshot.tokenSource ?? "none",
277
- running: snapshot.running ?? false,
278
- mode: snapshot.mode ?? null,
279
- lastStartAt: snapshot.lastStartAt ?? null,
280
- lastStopAt: snapshot.lastStopAt ?? null,
281
- lastError: snapshot.lastError ?? null,
282
- lastInboundAt: snapshot.lastInboundAt ?? null,
283
- lastOutboundAt: snapshot.lastOutboundAt ?? null,
284
- }),
285
- buildAccountSnapshot: ({ account, runtime }) => {
286
- const configured = Boolean(account.token?.trim() && account.appId?.trim());
287
- return {
288
- accountId: account.accountId,
289
- name: account.name,
290
- enabled: account.enabled,
291
- configured,
292
- tokenSource: account.tokenSource,
293
- baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
294
- running: runtime?.running ?? false,
295
- lastStartAt: runtime?.lastStartAt ?? null,
296
- lastStopAt: runtime?.lastStopAt ?? null,
297
- lastError: runtime?.lastError ?? null,
298
- mode: "webhook",
299
- lastInboundAt: runtime?.lastInboundAt ?? null,
300
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
301
- dmPolicy: account.config.dmPolicy ?? "pairing",
302
- };
303
- },
398
+ ...geweStatus,
304
399
  },
305
400
  gateway: {
306
401
  startAccount: async (ctx) => {
@@ -355,7 +450,7 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
355
450
 
356
451
  const accounts =
357
452
  nextSection.accounts && typeof nextSection.accounts === "object"
358
- ? { ...nextSection.accounts }
453
+ ? ({ ...nextSection.accounts } as Record<string, Record<string, unknown>>)
359
454
  : undefined;
360
455
  if (accounts && accountId in accounts) {
361
456
  const entry = accounts[accountId];
@@ -436,10 +531,12 @@ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
436
531
  const section = (namedConfig.channels?.[CHANNEL_CONFIG_KEY] ?? {}) as Record<
437
532
  string,
438
533
  unknown
439
- >;
534
+ > & {
535
+ accounts?: Record<string, Record<string, unknown>>;
536
+ };
440
537
  const useAccountPath = accountId !== DEFAULT_ACCOUNT_ID;
441
538
  const base = useAccountPath
442
- ? (section.accounts?.[accountId] as Record<string, unknown> | undefined) ?? {}
539
+ ? section.accounts?.[accountId] ?? {}
443
540
  : section;
444
541
  const nextEntry = {
445
542
  ...base,
@@ -0,0 +1,94 @@
1
+ import { CHANNEL_CONFIG_KEY } from "./constants.js";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
3
+
4
+ type ConfigWriteTarget =
5
+ | { kind: "channel"; scope: { channelId: string } }
6
+ | { kind: "account"; scope: { channelId: string; accountId: string } };
7
+
8
+ type GeweWriteSection = {
9
+ nextCfg: OpenClawConfig;
10
+ channelSection: Record<string, unknown>;
11
+ target: Record<string, unknown>;
12
+ pathPrefix: string;
13
+ writeTarget: ConfigWriteTarget;
14
+ };
15
+
16
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
18
+ return undefined;
19
+ }
20
+ return value as Record<string, unknown>;
21
+ }
22
+
23
+ export function cloneOpenClawConfig<T>(value: T): T {
24
+ return structuredClone(value);
25
+ }
26
+
27
+ function ensureGeweWriteSectionOnConfig(params: {
28
+ cfg: OpenClawConfig;
29
+ accountId?: string | null;
30
+ }): GeweWriteSection {
31
+ const nextCfg = params.cfg;
32
+ const channels = ((nextCfg.channels ??= {}) as Record<string, unknown>);
33
+ const channelSection = ((asRecord(channels[CHANNEL_CONFIG_KEY]) ?? {}) as Record<string, unknown>);
34
+ channels[CHANNEL_CONFIG_KEY] = channelSection;
35
+
36
+ const normalizedAccountId = normalizeAccountId(params.accountId);
37
+ const hasAccounts =
38
+ channelSection.accounts && typeof channelSection.accounts === "object" && !Array.isArray(channelSection.accounts);
39
+ const useAccount = normalizedAccountId !== DEFAULT_ACCOUNT_ID || Boolean(hasAccounts);
40
+ if (!useAccount) {
41
+ return {
42
+ nextCfg,
43
+ channelSection,
44
+ target: channelSection,
45
+ pathPrefix: `channels.${CHANNEL_CONFIG_KEY}`,
46
+ writeTarget: {
47
+ kind: "channel",
48
+ scope: { channelId: CHANNEL_CONFIG_KEY },
49
+ },
50
+ };
51
+ }
52
+
53
+ const accounts = ((asRecord(channelSection.accounts) ?? {}) as Record<string, unknown>);
54
+ channelSection.accounts = accounts;
55
+ const accountSection = ((asRecord(accounts[normalizedAccountId]) ?? {}) as Record<string, unknown>);
56
+ accounts[normalizedAccountId] = accountSection;
57
+ return {
58
+ nextCfg,
59
+ channelSection,
60
+ target: accountSection,
61
+ pathPrefix: `channels.${CHANNEL_CONFIG_KEY}.accounts.${normalizedAccountId}`,
62
+ writeTarget: {
63
+ kind: "account",
64
+ scope: {
65
+ channelId: CHANNEL_CONFIG_KEY,
66
+ accountId: normalizedAccountId,
67
+ },
68
+ },
69
+ };
70
+ }
71
+
72
+ export function ensureGeweWriteSection(params: {
73
+ cfg: OpenClawConfig;
74
+ accountId?: string | null;
75
+ }) {
76
+ return ensureGeweWriteSectionOnConfig({
77
+ cfg: cloneOpenClawConfig(params.cfg),
78
+ accountId: params.accountId,
79
+ });
80
+ }
81
+
82
+ export function ensureGeweWriteSectionInPlace(params: {
83
+ cfg: OpenClawConfig;
84
+ accountId?: string | null;
85
+ }) {
86
+ return ensureGeweWriteSectionOnConfig(params);
87
+ }
88
+
89
+ export function cleanupEmptyObject(parent: Record<string, unknown>, key: string) {
90
+ const value = asRecord(parent[key]);
91
+ if (value && Object.keys(value).length === 0) {
92
+ delete parent[key];
93
+ }
94
+ }
@@ -6,20 +6,94 @@ import {
6
6
  MarkdownConfigSchema,
7
7
  ToolPolicySchema,
8
8
  requireOpenAllowFrom,
9
- } from "openclaw/plugin-sdk";
9
+ } from "./openclaw-compat.js";
10
10
  import { z } from "zod";
11
11
 
12
+ const GeweGroupTriggerSchema = z
13
+ .object({
14
+ mode: z.enum(["at", "quote", "at_or_quote", "any_message"]).optional(),
15
+ })
16
+ .strict();
17
+
18
+ const GeweDmTriggerSchema = z
19
+ .object({
20
+ mode: z.enum(["any_message", "quote"]).optional(),
21
+ })
22
+ .strict();
23
+
24
+ const GeweGroupReplySchema = z
25
+ .object({
26
+ mode: z.enum(["plain", "quote_source", "at_sender", "quote_and_at"]).optional(),
27
+ })
28
+ .strict();
29
+
30
+ const GeweDmReplySchema = z
31
+ .object({
32
+ mode: z.enum(["plain", "quote_source"]).optional(),
33
+ })
34
+ .strict();
35
+
36
+ const GeweBindingIdentitySelfSchema = z
37
+ .object({
38
+ source: z.enum(["agent_name", "agent_id", "literal"]).optional(),
39
+ value: z.string().optional(),
40
+ })
41
+ .strict()
42
+ .superRefine((value, ctx) => {
43
+ if (value.source === "literal" && !value.value?.trim()) {
44
+ ctx.addIssue({
45
+ code: z.ZodIssueCode.custom,
46
+ path: ["value"],
47
+ message: "value is required when source=literal",
48
+ });
49
+ }
50
+ });
51
+
52
+ const GeweBindingIdentityRemarkSchema = z
53
+ .object({
54
+ source: z.enum(["agent_id", "agent_name", "name_and_id", "literal"]).optional(),
55
+ value: z.string().optional(),
56
+ })
57
+ .strict()
58
+ .superRefine((value, ctx) => {
59
+ if (value.source === "literal" && !value.value?.trim()) {
60
+ ctx.addIssue({
61
+ code: z.ZodIssueCode.custom,
62
+ path: ["value"],
63
+ message: "value is required when source=literal",
64
+ });
65
+ }
66
+ });
67
+
68
+ const GeweBindingIdentitySchema = z
69
+ .object({
70
+ enabled: z.boolean().optional(),
71
+ selfNickname: GeweBindingIdentitySelfSchema.optional(),
72
+ remark: GeweBindingIdentityRemarkSchema.optional(),
73
+ })
74
+ .strict();
75
+
12
76
  export const GeweGroupSchema = z
13
77
  .object({
14
78
  requireMention: z.boolean().optional(),
15
- tools: ToolPolicySchema,
79
+ tools: ToolPolicySchema.optional(),
16
80
  skills: z.array(z.string()).optional(),
17
81
  enabled: z.boolean().optional(),
18
82
  allowFrom: z.array(z.string()).optional(),
19
83
  systemPrompt: z.string().optional(),
84
+ trigger: GeweGroupTriggerSchema.optional(),
85
+ reply: GeweGroupReplySchema.optional(),
86
+ bindingIdentity: GeweBindingIdentitySchema.optional(),
20
87
  })
21
88
  .strict();
22
89
 
90
+ export const GeweDmSchema = DmConfigSchema.extend({
91
+ skills: z.array(z.string()).optional(),
92
+ systemPrompt: z.string().optional(),
93
+ trigger: GeweDmTriggerSchema.optional(),
94
+ reply: GeweDmReplySchema.optional(),
95
+ }).strict();
96
+
23
97
  export const GeweAccountSchemaBase = z
24
98
  .object({
25
99
  name: z.string().optional(),
@@ -80,9 +154,10 @@ export const GeweAccountSchemaBase = z
80
154
  groups: z.record(z.string(), GeweGroupSchema.optional()).optional(),
81
155
  historyLimit: z.number().int().min(0).optional(),
82
156
  dmHistoryLimit: z.number().int().min(0).optional(),
83
- dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
157
+ dms: z.record(z.string(), GeweDmSchema.optional()).optional(),
84
158
  textChunkLimit: z.number().int().positive().optional(),
85
159
  chunkMode: z.enum(["length", "newline"]).optional(),
160
+ autoQuoteReply: z.boolean().optional(),
86
161
  blockStreaming: z.boolean().optional(),
87
162
  blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
88
163
  })