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,150 @@
1
+ import type { ChannelAllowlistAdapter } from "openclaw/plugin-sdk/channel-runtime";
2
+
3
+ import { resolveGeweAccount } from "./accounts.js";
4
+ import { collectKnownGeweGroupEntries } from "./channel-directory.js";
5
+ import { cleanupEmptyObject, ensureGeweWriteSectionInPlace } from "./config-edit.js";
6
+ import { CHANNEL_CONFIG_KEY, stripChannelPrefix } from "./constants.js";
7
+ import { normalizeGeweMessagingTarget } from "./normalize.js";
8
+ import { normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
9
+ import type { CoreConfig } from "./types.js";
10
+ import { resolveCachedGeweName } from "./directory-cache.js";
11
+
12
+ function normalizeAllowEntry(raw: unknown): string | null {
13
+ const stripped = stripChannelPrefix(String(raw ?? "").trim());
14
+ if (!stripped || stripped === "*") {
15
+ return stripped === "*" ? "*" : null;
16
+ }
17
+ return normalizeGeweMessagingTarget(stripped) ?? stripped;
18
+ }
19
+
20
+ function dedupeEntries(values: Array<string | number>): string[] {
21
+ const seen = new Set<string>();
22
+ const result: string[] = [];
23
+ for (const value of values) {
24
+ const normalized = normalizeAllowEntry(value);
25
+ if (!normalized || seen.has(normalized)) {
26
+ continue;
27
+ }
28
+ seen.add(normalized);
29
+ result.push(normalized);
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function resolveOverridesLabel(params: {
35
+ accountId: string;
36
+ groupId: string;
37
+ cfg: OpenClawConfig;
38
+ }): string {
39
+ if (params.groupId === "*") {
40
+ return "*";
41
+ }
42
+ return (
43
+ resolveCachedGeweName({
44
+ accountId: params.accountId,
45
+ id: params.groupId,
46
+ kind: "group",
47
+ }) ?? params.groupId
48
+ );
49
+ }
50
+
51
+ function getWritableList(target: Record<string, unknown>, key: string): string[] {
52
+ return Array.isArray(target[key])
53
+ ? (target[key] as unknown[]).map((entry) => String(entry)).filter(Boolean)
54
+ : [];
55
+ }
56
+
57
+ export const geweAllowlist: ChannelAllowlistAdapter = {
58
+ supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all",
59
+ readConfig: ({ cfg, accountId }) => {
60
+ const account = resolveGeweAccount({
61
+ cfg: cfg as CoreConfig,
62
+ accountId,
63
+ });
64
+ const groupOverrides = Object.entries(account.config.groups ?? {})
65
+ .filter(([, value]) => Array.isArray(value?.allowFrom) && value.allowFrom.length > 0)
66
+ .map(([groupId, value]) => ({
67
+ label: resolveOverridesLabel({
68
+ accountId: account.accountId,
69
+ groupId,
70
+ cfg,
71
+ }),
72
+ entries: dedupeEntries((value?.allowFrom ?? []) as Array<string | number>),
73
+ }));
74
+ return {
75
+ dmAllowFrom: dedupeEntries(account.config.allowFrom ?? []),
76
+ groupAllowFrom: dedupeEntries(account.config.groupAllowFrom ?? []),
77
+ dmPolicy: account.config.dmPolicy,
78
+ groupPolicy: account.config.groupPolicy,
79
+ ...(groupOverrides.length > 0 ? { groupOverrides } : {}),
80
+ };
81
+ },
82
+ resolveNames: ({ cfg, accountId, scope, entries }) => {
83
+ const resolvedAccountId = resolveGeweAccount({
84
+ cfg: cfg as CoreConfig,
85
+ accountId,
86
+ }).accountId;
87
+ return entries.map((entry) => {
88
+ const normalized = normalizeAllowEntry(entry);
89
+ if (!normalized) {
90
+ return { input: entry, resolved: false, name: null };
91
+ }
92
+ const name =
93
+ scope === "group"
94
+ ? collectKnownGeweGroupEntries({ cfg, accountId: resolvedAccountId }).find(
95
+ (group) => group.id === normalized,
96
+ )?.name
97
+ : resolveCachedGeweName({
98
+ accountId: resolvedAccountId,
99
+ id: normalized,
100
+ kind: "user",
101
+ });
102
+ return {
103
+ input: entry,
104
+ resolved: Boolean(name),
105
+ name: name ?? null,
106
+ };
107
+ });
108
+ },
109
+ applyConfigEdit: ({ cfg, parsedConfig, accountId, scope, action, entry }) => {
110
+ const normalizedEntry = normalizeAllowEntry(entry);
111
+ if (!normalizedEntry) {
112
+ return { kind: "invalid-entry" };
113
+ }
114
+ const write = ensureGeweWriteSectionInPlace({
115
+ cfg: parsedConfig as OpenClawConfig,
116
+ accountId,
117
+ });
118
+ const key = scope === "dm" ? "allowFrom" : "groupAllowFrom";
119
+ const existing = dedupeEntries(getWritableList(write.target, key));
120
+ const nextSet = new Set(existing);
121
+ const hadEntry = nextSet.has(normalizedEntry);
122
+ if (action === "add") {
123
+ nextSet.add(normalizedEntry);
124
+ } else {
125
+ nextSet.delete(normalizedEntry);
126
+ }
127
+ const next = Array.from(nextSet);
128
+ const changed = action === "add" ? !hadEntry : hadEntry;
129
+ if (changed) {
130
+ if (next.length > 0) {
131
+ write.target[key] = next;
132
+ } else {
133
+ delete write.target[key];
134
+ }
135
+ if (write.writeTarget.kind === "account") {
136
+ const channels = write.nextCfg.channels as Record<string, unknown>;
137
+ const channel = channels[CHANNEL_CONFIG_KEY] as Record<string, unknown>;
138
+ const accounts = channel.accounts as Record<string, unknown>;
139
+ cleanupEmptyObject(accounts, normalizeAccountId(accountId));
140
+ cleanupEmptyObject(channel, "accounts");
141
+ }
142
+ }
143
+ return {
144
+ kind: "ok" as const,
145
+ changed,
146
+ pathLabel: `${write.pathPrefix}.${key}`,
147
+ writeTarget: write.writeTarget,
148
+ };
149
+ },
150
+ };
@@ -0,0 +1,419 @@
1
+ import type { ChannelDirectoryAdapter, ChannelDirectoryEntry } from "openclaw/plugin-sdk/channel-runtime";
2
+
3
+ import { resolveGeweAccount } from "./accounts.js";
4
+ import {
5
+ fetchContactsListCacheGewe,
6
+ fetchContactsListGewe,
7
+ getBriefInfoGewe,
8
+ type GeweContactProfile,
9
+ type GeweContactsCatalog,
10
+ } from "./contacts-api.js";
11
+ import {
12
+ getGeweChatroomInfo,
13
+ getGeweProfile,
14
+ normalizeGeweBindingConversationId,
15
+ } from "./group-binding.js";
16
+ import { normalizeGeweMessagingTarget } from "./normalize.js";
17
+ import { normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
18
+ import type { CoreConfig } from "./types.js";
19
+ import {
20
+ listCachedGeweGroups,
21
+ listCachedGeweUsers,
22
+ rememberGeweDirectoryObservation,
23
+ rememberGeweGroupMembers,
24
+ rememberGeweUsers,
25
+ resolveCachedGeweName,
26
+ } from "./directory-cache.js";
27
+
28
+ type DirectoryNamedEntry = {
29
+ id: string;
30
+ name?: string;
31
+ };
32
+
33
+ type BindingLike = {
34
+ match?: {
35
+ channel?: string;
36
+ accountId?: string;
37
+ peer?: {
38
+ kind?: string;
39
+ id?: string;
40
+ };
41
+ };
42
+ };
43
+
44
+ const CHANNEL_ALIASES = new Set(["gewe-openclaw", "gewe", "wechat", "wx"]);
45
+ const BRIEF_INFO_BATCH_SIZE = 100;
46
+
47
+ function addNamedEntry(target: Map<string, DirectoryNamedEntry>, entry: DirectoryNamedEntry) {
48
+ if (!entry.id || target.has(entry.id)) {
49
+ return;
50
+ }
51
+ target.set(entry.id, entry);
52
+ }
53
+
54
+ function normalizeQuery(value?: string | null): string {
55
+ return value?.trim().toLowerCase() || "";
56
+ }
57
+
58
+ function matchesQuery(entry: DirectoryNamedEntry, query: string): boolean {
59
+ if (!query) {
60
+ return true;
61
+ }
62
+ return entry.id.toLowerCase().includes(query) || entry.name?.toLowerCase().includes(query) === true;
63
+ }
64
+
65
+ function applyQueryAndLimit(
66
+ entries: DirectoryNamedEntry[],
67
+ params: { query?: string | null; limit?: number | null },
68
+ ): DirectoryNamedEntry[] {
69
+ const query = normalizeQuery(params.query);
70
+ const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : undefined;
71
+ const filtered = entries.filter((entry) => matchesQuery(entry, query));
72
+ return typeof limit === "number" ? filtered.slice(0, limit) : filtered;
73
+ }
74
+
75
+ function toDirectoryEntries(
76
+ kind: "user" | "group",
77
+ entries: DirectoryNamedEntry[],
78
+ ): ChannelDirectoryEntry[] {
79
+ return entries.map((entry) => ({
80
+ kind,
81
+ id: entry.id,
82
+ name: entry.name,
83
+ }));
84
+ }
85
+
86
+ function chunkEntries<T>(values: T[], size: number): T[][] {
87
+ if (values.length === 0) {
88
+ return [];
89
+ }
90
+ const chunks: T[][] = [];
91
+ for (let index = 0; index < values.length; index += size) {
92
+ chunks.push(values.slice(index, index + size));
93
+ }
94
+ return chunks;
95
+ }
96
+
97
+ function isGroupId(value: string): boolean {
98
+ return /@chatroom$/i.test(value);
99
+ }
100
+
101
+ function dedupeIds(values: Array<string | null | undefined>): string[] {
102
+ const seen = new Set<string>();
103
+ const result: string[] = [];
104
+ for (const value of values) {
105
+ const normalized = normalizeGeweMessagingTarget(value ?? "");
106
+ if (!normalized || seen.has(normalized)) {
107
+ continue;
108
+ }
109
+ seen.add(normalized);
110
+ result.push(normalized);
111
+ }
112
+ return result;
113
+ }
114
+
115
+ function resolveCatalogFriendIds(catalog: GeweContactsCatalog | undefined): string[] {
116
+ return dedupeIds(catalog?.friends ?? []);
117
+ }
118
+
119
+ function resolveContactProfileId(profile: GeweContactProfile): string | undefined {
120
+ const normalized = normalizeGeweMessagingTarget(
121
+ String(profile.userName ?? profile.wxid ?? "").trim(),
122
+ );
123
+ if (!normalized || isGroupId(normalized)) {
124
+ return undefined;
125
+ }
126
+ return normalized;
127
+ }
128
+
129
+ function resolveContactDisplayName(profile: GeweContactProfile): string | undefined {
130
+ const candidates = [profile.remark, profile.nickName, profile.alias];
131
+ for (const candidate of candidates) {
132
+ const trimmed = typeof candidate === "string" ? candidate.trim() : "";
133
+ if (trimmed) {
134
+ return trimmed;
135
+ }
136
+ }
137
+ return undefined;
138
+ }
139
+
140
+ function listConfigBindings(cfg: OpenClawConfig): BindingLike[] {
141
+ return Array.isArray(cfg.bindings) ? (cfg.bindings as BindingLike[]) : [];
142
+ }
143
+
144
+ function normalizeBindingChannel(value?: string): string {
145
+ const trimmed = value?.trim().toLowerCase() ?? "";
146
+ return CHANNEL_ALIASES.has(trimmed) ? "gewe-openclaw" : trimmed;
147
+ }
148
+
149
+ function bindingMatchesAccount(bindingAccountId: string | undefined, accountId: string): boolean {
150
+ const trimmed = bindingAccountId?.trim();
151
+ if (!trimmed) {
152
+ return accountId === "default";
153
+ }
154
+ if (trimmed === "*") {
155
+ return true;
156
+ }
157
+ return normalizeAccountId(trimmed) === accountId;
158
+ }
159
+
160
+ function collectKnownPeerEntries(params: {
161
+ cfg: OpenClawConfig;
162
+ accountId?: string | null;
163
+ }): DirectoryNamedEntry[] {
164
+ const account = resolveGeweAccount({
165
+ cfg: params.cfg as CoreConfig,
166
+ accountId: params.accountId,
167
+ });
168
+ const entries = new Map<string, DirectoryNamedEntry>();
169
+ const pushUserId = (raw: unknown) => {
170
+ const normalized = normalizeGeweMessagingTarget(String(raw ?? ""));
171
+ if (!normalized || normalized === "*" || isGroupId(normalized)) {
172
+ return;
173
+ }
174
+ addNamedEntry(entries, {
175
+ id: normalized,
176
+ name: resolveCachedGeweName({
177
+ accountId: account.accountId,
178
+ id: normalized,
179
+ kind: "user",
180
+ }),
181
+ });
182
+ };
183
+
184
+ for (const entry of account.config.allowFrom ?? []) {
185
+ pushUserId(entry);
186
+ }
187
+ for (const id of Object.keys(account.config.dms ?? {})) {
188
+ pushUserId(id);
189
+ }
190
+ for (const entry of account.config.groupAllowFrom ?? []) {
191
+ pushUserId(entry);
192
+ }
193
+ for (const cached of listCachedGeweUsers(account.accountId)) {
194
+ addNamedEntry(entries, {
195
+ id: cached.id,
196
+ name: cached.name,
197
+ });
198
+ }
199
+ return Array.from(entries.values());
200
+ }
201
+
202
+ function collectKnownGroupEntries(params: {
203
+ cfg: OpenClawConfig;
204
+ accountId?: string | null;
205
+ }): DirectoryNamedEntry[] {
206
+ const account = resolveGeweAccount({
207
+ cfg: params.cfg as CoreConfig,
208
+ accountId: params.accountId,
209
+ });
210
+ const entries = new Map<string, DirectoryNamedEntry>();
211
+
212
+ for (const binding of listConfigBindings(params.cfg)) {
213
+ if (normalizeBindingChannel(binding.match?.channel) !== "gewe-openclaw") {
214
+ continue;
215
+ }
216
+ if (!bindingMatchesAccount(binding.match?.accountId, account.accountId)) {
217
+ continue;
218
+ }
219
+ if (binding.match?.peer?.kind?.trim().toLowerCase() !== "group") {
220
+ continue;
221
+ }
222
+ const groupId = normalizeGeweBindingConversationId(binding.match.peer.id);
223
+ if (!groupId || groupId === "*") {
224
+ continue;
225
+ }
226
+ addNamedEntry(entries, {
227
+ id: groupId,
228
+ name: resolveCachedGeweName({
229
+ accountId: account.accountId,
230
+ id: groupId,
231
+ kind: "group",
232
+ }),
233
+ });
234
+ }
235
+
236
+ for (const groupId of Object.keys(account.config.groups ?? {})) {
237
+ if (groupId === "*") {
238
+ continue;
239
+ }
240
+ const normalized = normalizeGeweBindingConversationId(groupId);
241
+ if (!normalized) {
242
+ continue;
243
+ }
244
+ addNamedEntry(entries, {
245
+ id: normalized,
246
+ name: resolveCachedGeweName({
247
+ accountId: account.accountId,
248
+ id: normalized,
249
+ kind: "group",
250
+ }),
251
+ });
252
+ }
253
+
254
+ for (const cached of listCachedGeweGroups(account.accountId)) {
255
+ addNamedEntry(entries, {
256
+ id: cached.id,
257
+ name: cached.name,
258
+ });
259
+ }
260
+ return Array.from(entries.values());
261
+ }
262
+
263
+ export function collectKnownGewePeerEntries(params: {
264
+ cfg: OpenClawConfig;
265
+ accountId?: string | null;
266
+ }) {
267
+ return collectKnownPeerEntries(params);
268
+ }
269
+
270
+ export function collectKnownGeweGroupEntries(params: {
271
+ cfg: OpenClawConfig;
272
+ accountId?: string | null;
273
+ }) {
274
+ return collectKnownGroupEntries(params);
275
+ }
276
+
277
+ async function enrichPeerEntriesFromContacts(params: {
278
+ cfg: OpenClawConfig;
279
+ accountId?: string | null;
280
+ entries: DirectoryNamedEntry[];
281
+ }): Promise<DirectoryNamedEntry[]> {
282
+ const account = resolveGeweAccount({
283
+ cfg: params.cfg as CoreConfig,
284
+ accountId: params.accountId,
285
+ });
286
+ const entries = new Map<string, DirectoryNamedEntry>();
287
+ for (const entry of params.entries) {
288
+ addNamedEntry(entries, entry);
289
+ }
290
+
291
+ try {
292
+ const cachedCatalog = await fetchContactsListCacheGewe({ account });
293
+ let friendIds = resolveCatalogFriendIds(cachedCatalog);
294
+ if (friendIds.length === 0) {
295
+ friendIds = resolveCatalogFriendIds(await fetchContactsListGewe({ account }));
296
+ }
297
+
298
+ for (const friendId of friendIds) {
299
+ addNamedEntry(entries, {
300
+ id: friendId,
301
+ name: resolveCachedGeweName({
302
+ accountId: account.accountId,
303
+ id: friendId,
304
+ kind: "user",
305
+ }),
306
+ });
307
+ }
308
+
309
+ const missingIds = Array.from(entries.values())
310
+ .filter((entry) => !entry.name && !isGroupId(entry.id))
311
+ .map((entry) => entry.id);
312
+ if (missingIds.length === 0) {
313
+ return Array.from(entries.values());
314
+ }
315
+
316
+ const rememberedUsers: Array<{ id: string; name?: string }> = [];
317
+ for (const batch of chunkEntries(missingIds, BRIEF_INFO_BATCH_SIZE)) {
318
+ const profiles = (await getBriefInfoGewe({ account, wxids: batch })) ?? [];
319
+ for (const profile of profiles) {
320
+ const id = resolveContactProfileId(profile);
321
+ if (!id) {
322
+ continue;
323
+ }
324
+ const name = resolveContactDisplayName(profile);
325
+ rememberedUsers.push({ id, name });
326
+ const existing = entries.get(id);
327
+ if (existing) {
328
+ entries.set(id, {
329
+ ...existing,
330
+ name: name ?? existing.name,
331
+ });
332
+ continue;
333
+ }
334
+ addNamedEntry(entries, { id, name });
335
+ }
336
+ }
337
+
338
+ if (rememberedUsers.length > 0) {
339
+ rememberGeweUsers({
340
+ accountId: account.accountId,
341
+ users: rememberedUsers,
342
+ });
343
+ }
344
+ } catch {
345
+ // 目录 enrich 走尽力而为策略,避免因为通讯录 API 波动导致目录不可用。
346
+ }
347
+
348
+ return Array.from(entries.values());
349
+ }
350
+
351
+ export const geweDirectory: ChannelDirectoryAdapter = {
352
+ self: async ({ cfg, accountId }) => {
353
+ const account = resolveGeweAccount({
354
+ cfg: cfg as CoreConfig,
355
+ accountId,
356
+ });
357
+ const profile = await getGeweProfile({ account });
358
+ return {
359
+ kind: "user",
360
+ id: profile.wxid,
361
+ name: profile.nickName,
362
+ raw: profile,
363
+ };
364
+ },
365
+ listPeers: async ({ cfg, accountId, query, limit }) =>
366
+ toDirectoryEntries(
367
+ "user",
368
+ applyQueryAndLimit(
369
+ await enrichPeerEntriesFromContacts({
370
+ cfg,
371
+ accountId,
372
+ entries: collectKnownPeerEntries({ cfg, accountId }),
373
+ }),
374
+ { query, limit },
375
+ ),
376
+ ),
377
+ listGroups: async ({ cfg, accountId, query, limit }) =>
378
+ toDirectoryEntries(
379
+ "group",
380
+ applyQueryAndLimit(collectKnownGroupEntries({ cfg, accountId }), { query, limit }),
381
+ ),
382
+ listGroupMembers: async ({ cfg, accountId, groupId, limit }) => {
383
+ const account = resolveGeweAccount({
384
+ cfg: cfg as CoreConfig,
385
+ accountId,
386
+ });
387
+ const groupInfo = await getGeweChatroomInfo({
388
+ account,
389
+ groupId,
390
+ });
391
+ const members = groupInfo.memberList?.map((member) => ({
392
+ id: member.wxid?.trim(),
393
+ name: member.displayName?.trim() || member.nickName?.trim() || undefined,
394
+ })) ?? [];
395
+ rememberGeweDirectoryObservation({
396
+ accountId: account.accountId,
397
+ groupId: groupInfo.chatroomId,
398
+ groupName: groupInfo.nickName,
399
+ });
400
+ rememberGeweGroupMembers({
401
+ accountId: account.accountId,
402
+ groupId: groupInfo.chatroomId,
403
+ groupName: groupInfo.nickName,
404
+ members,
405
+ });
406
+ return toDirectoryEntries(
407
+ "user",
408
+ applyQueryAndLimit(
409
+ members
410
+ .filter((member): member is DirectoryNamedEntry => Boolean(member.id))
411
+ .map((member) => ({
412
+ id: member.id!,
413
+ name: member.name,
414
+ })),
415
+ { limit },
416
+ ),
417
+ );
418
+ },
419
+ };