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,478 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./openclaw-compat.js";
6
+ import { CHANNEL_ID, stripChannelPrefix } from "./constants.js";
7
+ import { resolveOpenClawStateDir } from "./state-paths.js";
8
+
9
+ const PAIR_CODE_TTL_MS = 60 * 60 * 1000;
10
+ const LOCK_STALE_MS = 30_000;
11
+ const LOCK_RETRY_ATTEMPTS = 12;
12
+ const LOCK_RETRY_BASE_MS = 50;
13
+
14
+ type AllowFromStore = {
15
+ version: 1;
16
+ allowFrom: string[];
17
+ };
18
+
19
+ type GewePairCodeEntry = {
20
+ code: string;
21
+ accountId?: string;
22
+ createdAt?: string;
23
+ };
24
+
25
+ type GewePairCodeStore = {
26
+ version: 1;
27
+ codes: Array<string | GewePairCodeEntry>;
28
+ };
29
+
30
+ type LegacyPairingRequest = {
31
+ id?: string;
32
+ code?: string;
33
+ createdAt?: string;
34
+ lastSeenAt?: string;
35
+ meta?: Record<string, unknown>;
36
+ };
37
+
38
+ type LegacyPairingStore = {
39
+ version: 1;
40
+ requests: LegacyPairingRequest[];
41
+ };
42
+
43
+ type CanonicalPairCodeEntry = {
44
+ code: string;
45
+ accountId: string;
46
+ createdAt?: string;
47
+ };
48
+
49
+ type CanonicalLegacyPairingRequest = {
50
+ code: string;
51
+ accountId: string;
52
+ createdAt?: string;
53
+ persisted: LegacyPairingRequest;
54
+ };
55
+
56
+ function resolveCredentialsDir(env: NodeJS.ProcessEnv = process.env): string {
57
+ return path.join(resolveOpenClawStateDir(env), "credentials");
58
+ }
59
+
60
+ function safeChannelKey(channel: string): string {
61
+ const raw = channel.trim().toLowerCase();
62
+ if (!raw) {
63
+ throw new Error("invalid pairing channel");
64
+ }
65
+ const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
66
+ if (!safe || safe === "_") {
67
+ throw new Error("invalid pairing channel");
68
+ }
69
+ return safe;
70
+ }
71
+
72
+ function safeAccountKey(accountId: string): string {
73
+ const raw = accountId.trim().toLowerCase();
74
+ if (!raw) {
75
+ throw new Error("invalid pairing account id");
76
+ }
77
+ const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
78
+ if (!safe || safe === "_") {
79
+ throw new Error("invalid pairing account id");
80
+ }
81
+ return safe;
82
+ }
83
+
84
+ function normalizeStoreAccountId(accountId?: string): string {
85
+ return normalizeAccountId(accountId);
86
+ }
87
+
88
+ function normalizePairCode(code: string | undefined | null): string {
89
+ return (code ?? "").trim().toUpperCase();
90
+ }
91
+
92
+ function normalizeAllowFromEntry(entry: string | number): string {
93
+ const normalized = stripChannelPrefix(String(entry).trim());
94
+ if (!normalized || normalized === "*") {
95
+ return "";
96
+ }
97
+ return normalized;
98
+ }
99
+
100
+ function dedupeEntries(entries: string[]): string[] {
101
+ const seen = new Set<string>();
102
+ const next: string[] = [];
103
+ for (const entry of entries) {
104
+ const normalized = entry.trim();
105
+ if (!normalized || seen.has(normalized)) {
106
+ continue;
107
+ }
108
+ seen.add(normalized);
109
+ next.push(normalized);
110
+ }
111
+ return next;
112
+ }
113
+
114
+ function isExpired(createdAt: string | undefined, nowMs: number, allowMissing = false): boolean {
115
+ if (!createdAt) {
116
+ return !allowMissing;
117
+ }
118
+ const parsed = Date.parse(createdAt);
119
+ if (!Number.isFinite(parsed)) {
120
+ return true;
121
+ }
122
+ return nowMs - parsed > PAIR_CODE_TTL_MS;
123
+ }
124
+
125
+ async function readJsonFileWithFallback<T>(
126
+ filePath: string,
127
+ fallback: T,
128
+ ): Promise<{ value: T; exists: boolean }> {
129
+ try {
130
+ const raw = await fs.readFile(filePath, "utf8");
131
+ const parsed = JSON.parse(raw) as T | null;
132
+ return {
133
+ value: parsed ?? fallback,
134
+ exists: true,
135
+ };
136
+ } catch (err) {
137
+ const code = (err as NodeJS.ErrnoException).code;
138
+ if (code === "ENOENT") {
139
+ return {
140
+ value: fallback,
141
+ exists: false,
142
+ };
143
+ }
144
+ return {
145
+ value: fallback,
146
+ exists: true,
147
+ };
148
+ }
149
+ }
150
+
151
+ async function writeJsonFileAtomically(filePath: string, value: unknown): Promise<void> {
152
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
153
+ const tmpPath = path.join(
154
+ path.dirname(filePath),
155
+ `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`,
156
+ );
157
+ await fs.writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
158
+ await fs.chmod(tmpPath, 0o600).catch(() => {});
159
+ await fs.rename(tmpPath, filePath);
160
+ }
161
+
162
+ async function isStaleLock(lockPath: string): Promise<boolean> {
163
+ const stat = await fs.stat(lockPath).catch((err: NodeJS.ErrnoException) => {
164
+ if (err.code === "ENOENT") {
165
+ return null;
166
+ }
167
+ throw err;
168
+ });
169
+ if (!stat) {
170
+ return true;
171
+ }
172
+ return Date.now() - stat.mtimeMs > LOCK_STALE_MS;
173
+ }
174
+
175
+ async function sleep(ms: number): Promise<void> {
176
+ await new Promise((resolve) => setTimeout(resolve, ms));
177
+ }
178
+
179
+ async function withFileLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
180
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
181
+ const lockPath = `${filePath}.lock`;
182
+ for (let attempt = 0; attempt < LOCK_RETRY_ATTEMPTS; attempt += 1) {
183
+ try {
184
+ const handle = await fs.open(lockPath, "wx");
185
+ try {
186
+ return await fn();
187
+ } finally {
188
+ await handle.close().catch(() => {});
189
+ await fs.rm(lockPath, { force: true }).catch(() => {});
190
+ }
191
+ } catch (err) {
192
+ const code = (err as NodeJS.ErrnoException).code;
193
+ if (code !== "EEXIST") {
194
+ throw err;
195
+ }
196
+ if (await isStaleLock(lockPath)) {
197
+ await fs.rm(lockPath, { force: true }).catch(() => {});
198
+ continue;
199
+ }
200
+ if (attempt === LOCK_RETRY_ATTEMPTS - 1) {
201
+ throw new Error(`file lock timeout for ${filePath}`);
202
+ }
203
+ const delayMs = Math.min(
204
+ LOCK_RETRY_BASE_MS * 2 ** Math.min(attempt, 5),
205
+ 1_000,
206
+ );
207
+ await sleep(delayMs);
208
+ }
209
+ }
210
+ throw new Error(`file lock timeout for ${filePath}`);
211
+ }
212
+
213
+ async function readAllowFromFile(filePath: string): Promise<string[]> {
214
+ const { value } = await readJsonFileWithFallback<AllowFromStore>(filePath, {
215
+ version: 1,
216
+ allowFrom: [],
217
+ });
218
+ const allowFrom = Array.isArray(value.allowFrom) ? value.allowFrom : [];
219
+ return dedupeEntries(allowFrom.map(normalizeAllowFromEntry).filter(Boolean));
220
+ }
221
+
222
+ async function addAllowFromEntry(params: {
223
+ accountId?: string;
224
+ entry: string;
225
+ env?: NodeJS.ProcessEnv;
226
+ }): Promise<{ changed: boolean; allowFrom: string[] }> {
227
+ const filePath = resolveGeweAllowFromPath(params.accountId, params.env);
228
+ const normalizedEntry = normalizeAllowFromEntry(params.entry);
229
+ if (!normalizedEntry) {
230
+ return { changed: false, allowFrom: [] };
231
+ }
232
+
233
+ return await withFileLock(filePath, async () => {
234
+ const current = await readAllowFromFile(filePath);
235
+ if (current.includes(normalizedEntry)) {
236
+ return { changed: false, allowFrom: current };
237
+ }
238
+ const allowFrom = [...current, normalizedEntry];
239
+ await writeJsonFileAtomically(filePath, {
240
+ version: 1,
241
+ allowFrom,
242
+ } satisfies AllowFromStore);
243
+ return { changed: true, allowFrom };
244
+ });
245
+ }
246
+
247
+ function canonicalizePairCodeEntry(raw: string | GewePairCodeEntry): CanonicalPairCodeEntry | null {
248
+ if (typeof raw === "string") {
249
+ const code = normalizePairCode(raw);
250
+ if (!code) {
251
+ return null;
252
+ }
253
+ return {
254
+ code,
255
+ accountId: DEFAULT_ACCOUNT_ID,
256
+ };
257
+ }
258
+ if (!raw || typeof raw !== "object") {
259
+ return null;
260
+ }
261
+ const code = normalizePairCode(raw.code);
262
+ if (!code) {
263
+ return null;
264
+ }
265
+ return {
266
+ code,
267
+ accountId: normalizeStoreAccountId(raw.accountId),
268
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : undefined,
269
+ };
270
+ }
271
+
272
+ function persistPairCodeEntry(entry: CanonicalPairCodeEntry): GewePairCodeEntry {
273
+ return {
274
+ code: entry.code,
275
+ ...(entry.accountId !== DEFAULT_ACCOUNT_ID ? { accountId: entry.accountId } : {}),
276
+ ...(entry.createdAt ? { createdAt: entry.createdAt } : {}),
277
+ };
278
+ }
279
+
280
+ function canonicalizeLegacyPairingRequest(
281
+ raw: LegacyPairingRequest,
282
+ ): CanonicalLegacyPairingRequest | null {
283
+ const code = normalizePairCode(raw.code);
284
+ if (!code) {
285
+ return null;
286
+ }
287
+ const meta = raw.meta && typeof raw.meta === "object" ? raw.meta : {};
288
+ const accountIdRaw = typeof meta.accountId === "string" ? meta.accountId : undefined;
289
+ return {
290
+ code,
291
+ accountId: accountIdRaw ? normalizeStoreAccountId(accountIdRaw) : DEFAULT_ACCOUNT_ID,
292
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : undefined,
293
+ persisted: {
294
+ ...raw,
295
+ code,
296
+ meta,
297
+ },
298
+ };
299
+ }
300
+
301
+ async function redeemFromPairCodesStore(params: {
302
+ accountId?: string;
303
+ code: string;
304
+ id: string;
305
+ env?: NodeJS.ProcessEnv;
306
+ }): Promise<{ id: string; code: string; source: "pair-codes" } | null> {
307
+ const resolvedAccountId = normalizeStoreAccountId(params.accountId);
308
+ const targetCode = normalizePairCode(params.code);
309
+ const filePath = resolveGewePairCodesPath(params.env);
310
+
311
+ const matched = await withFileLock(filePath, async () => {
312
+ const { value, exists } = await readJsonFileWithFallback<GewePairCodeStore>(filePath, {
313
+ version: 1,
314
+ codes: [],
315
+ });
316
+ const nowMs = Date.now();
317
+ const codes = Array.isArray(value.codes) ? value.codes : [];
318
+ let found = false;
319
+ let changed = false;
320
+ const nextCodes: GewePairCodeEntry[] = [];
321
+ for (const raw of codes) {
322
+ const entry = canonicalizePairCodeEntry(raw);
323
+ if (!entry) {
324
+ changed = true;
325
+ continue;
326
+ }
327
+ if (isExpired(entry.createdAt, nowMs, true)) {
328
+ changed = true;
329
+ continue;
330
+ }
331
+ if (!found && entry.code === targetCode && entry.accountId === resolvedAccountId) {
332
+ found = true;
333
+ changed = true;
334
+ continue;
335
+ }
336
+ nextCodes.push(persistPairCodeEntry(entry));
337
+ }
338
+
339
+ if ((found || changed) && (exists || nextCodes.length > 0)) {
340
+ await writeJsonFileAtomically(filePath, {
341
+ version: 1,
342
+ codes: nextCodes,
343
+ } satisfies GewePairCodeStore);
344
+ }
345
+ return found;
346
+ });
347
+
348
+ if (!matched) {
349
+ return null;
350
+ }
351
+
352
+ await addAllowFromEntry({
353
+ accountId: resolvedAccountId,
354
+ entry: params.id,
355
+ env: params.env,
356
+ });
357
+ return {
358
+ id: params.id,
359
+ code: targetCode,
360
+ source: "pair-codes",
361
+ };
362
+ }
363
+
364
+ async function redeemFromLegacyPairingStore(params: {
365
+ accountId?: string;
366
+ code: string;
367
+ id: string;
368
+ env?: NodeJS.ProcessEnv;
369
+ }): Promise<{ id: string; code: string; source: "legacy-pairing" } | null> {
370
+ const resolvedAccountId = normalizeStoreAccountId(params.accountId);
371
+ const targetCode = normalizePairCode(params.code);
372
+ const filePath = resolveGeweLegacyPairingPath(params.env);
373
+
374
+ const matched = await withFileLock(filePath, async () => {
375
+ const { value, exists } = await readJsonFileWithFallback<LegacyPairingStore>(filePath, {
376
+ version: 1,
377
+ requests: [],
378
+ });
379
+ const nowMs = Date.now();
380
+ const requests = Array.isArray(value.requests) ? value.requests : [];
381
+ let found = false;
382
+ let changed = false;
383
+ const nextRequests: LegacyPairingRequest[] = [];
384
+ for (const raw of requests) {
385
+ const entry = canonicalizeLegacyPairingRequest(raw);
386
+ if (!entry) {
387
+ changed = true;
388
+ continue;
389
+ }
390
+ if (isExpired(entry.createdAt, nowMs, false)) {
391
+ changed = true;
392
+ continue;
393
+ }
394
+ if (!found && entry.code === targetCode && entry.accountId === resolvedAccountId) {
395
+ found = true;
396
+ changed = true;
397
+ continue;
398
+ }
399
+ nextRequests.push(entry.persisted);
400
+ }
401
+
402
+ if ((found || changed) && (exists || nextRequests.length > 0)) {
403
+ await writeJsonFileAtomically(filePath, {
404
+ version: 1,
405
+ requests: nextRequests,
406
+ } satisfies LegacyPairingStore);
407
+ }
408
+ return found;
409
+ });
410
+
411
+ if (!matched) {
412
+ return null;
413
+ }
414
+
415
+ await addAllowFromEntry({
416
+ accountId: resolvedAccountId,
417
+ entry: params.id,
418
+ env: params.env,
419
+ });
420
+ return {
421
+ id: params.id,
422
+ code: targetCode,
423
+ source: "legacy-pairing",
424
+ };
425
+ }
426
+
427
+ export function resolveGeweAllowFromPath(
428
+ accountId?: string,
429
+ env: NodeJS.ProcessEnv = process.env,
430
+ ): string {
431
+ return path.join(
432
+ resolveCredentialsDir(env),
433
+ `${safeChannelKey(CHANNEL_ID)}-${safeAccountKey(normalizeStoreAccountId(accountId))}-allowFrom.json`,
434
+ );
435
+ }
436
+
437
+ export function resolveGeweLegacyAllowFromPath(
438
+ env: NodeJS.ProcessEnv = process.env,
439
+ ): string {
440
+ return path.join(resolveCredentialsDir(env), `${safeChannelKey(CHANNEL_ID)}-allowFrom.json`);
441
+ }
442
+
443
+ export function resolveGewePairCodesPath(
444
+ env: NodeJS.ProcessEnv = process.env,
445
+ ): string {
446
+ return path.join(resolveCredentialsDir(env), `${safeChannelKey(CHANNEL_ID)}-pair-codes.json`);
447
+ }
448
+
449
+ export function resolveGeweLegacyPairingPath(
450
+ env: NodeJS.ProcessEnv = process.env,
451
+ ): string {
452
+ return path.join(resolveCredentialsDir(env), `${safeChannelKey(CHANNEL_ID)}-pairing.json`);
453
+ }
454
+
455
+ export async function readGeweAllowFromStore(params: {
456
+ accountId?: string;
457
+ env?: NodeJS.ProcessEnv;
458
+ }): Promise<string[]> {
459
+ const resolvedAccountId = normalizeStoreAccountId(params.accountId);
460
+ const scoped = await readAllowFromFile(resolveGeweAllowFromPath(resolvedAccountId, params.env));
461
+ if (resolvedAccountId !== DEFAULT_ACCOUNT_ID) {
462
+ return scoped;
463
+ }
464
+ const legacy = await readAllowFromFile(resolveGeweLegacyAllowFromPath(params.env));
465
+ return dedupeEntries([...scoped, ...legacy]);
466
+ }
467
+
468
+ export async function redeemGewePairCode(params: {
469
+ accountId?: string;
470
+ code: string;
471
+ id: string;
472
+ env?: NodeJS.ProcessEnv;
473
+ }): Promise<{ id: string; code: string; source: "pair-codes" | "legacy-pairing" } | null> {
474
+ return (
475
+ (await redeemFromPairCodesStore(params)) ??
476
+ (await redeemFromLegacyPairingStore(params))
477
+ );
478
+ }
@@ -0,0 +1,45 @@
1
+ import { createGeweAccountMethod, type GeweApiObject } from "./gewe-account-api.js";
2
+
3
+ export type GeweProfile = {
4
+ wxid: string;
5
+ nickName?: string;
6
+ } & GeweApiObject;
7
+
8
+ export const updateProfileGewe = createGeweAccountMethod<{
9
+ country: string;
10
+ province: string;
11
+ nickName: string;
12
+ sex: number;
13
+ signature: string;
14
+ city?: string;
15
+ }>("/gewe/v2/api/personal/updateProfile");
16
+
17
+ export const updateHeadImgGewe = createGeweAccountMethod<{
18
+ headImgUrl: string;
19
+ }>("/gewe/v2/api/personal/updateHeadImg");
20
+
21
+ export const getProfileGewe = createGeweAccountMethod<Record<string, never>, GeweProfile>(
22
+ "/gewe/v2/api/personal/getProfile",
23
+ );
24
+
25
+ export const getQrCodeGewe = createGeweAccountMethod<Record<string, never>>(
26
+ "/gewe/v2/api/personal/getQrCode",
27
+ );
28
+
29
+ export const getSafetyInfoGewe = createGeweAccountMethod<Record<string, never>>(
30
+ "/gewe/v2/api/personal/getSafetyInfo",
31
+ );
32
+
33
+ export const privacySettingsGewe = createGeweAccountMethod<{
34
+ open: boolean;
35
+ option?: number;
36
+ }>("/gewe/v2/api/personal/privacySettings");
37
+
38
+ export const gewePersonalApi = {
39
+ updateProfile: updateProfileGewe,
40
+ updateHeadImg: updateHeadImgGewe,
41
+ getProfile: getProfileGewe,
42
+ getQrCode: getQrCodeGewe,
43
+ getSafetyInfo: getSafetyInfoGewe,
44
+ privacySettings: privacySettingsGewe,
45
+ };