gewe-openclaw 2026.3.25 → 2026.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,7 +95,7 @@ openclaw onboard
95
95
  - `s3PublicBaseUrl`:`public` 模式下用于拼接可访问 URL(必填)。
96
96
  - `s3PresignExpiresSec`:`presigned` 模式签名有效期(默认 3600 秒)。
97
97
  - `s3KeyPrefix`:对象 key 前缀(默认 `gewe-openclaw/outbound`)。
98
- - `allowFrom`:允许私聊触发的微信 ID(或在群里走 allowlist 规则)。
98
+ - `allowFrom`:允许私聊触发的微信 ID;不再隐式用于群聊权限。
99
99
  - `voiceAutoConvert`:自动将音频转为 silk(默认开启;设为 `false` 可关闭)。
100
100
  - `silkAutoDownload`:自动下载 `rust-silk`(默认开启;可关闭后自行配置 `voiceSilkPath` / `voiceDecodePath`)。
101
101
  - `silkVersion`:自动下载的 `rust-silk` 版本(`latest` 会自动清理旧版本)。
@@ -238,6 +238,11 @@ GeWe 现在补齐了目录、标准 allowlist 适配和状态摘要。
238
238
  - 私聊:`allowFrom`
239
239
  - 群发言人:`groupAllowFrom`
240
240
 
241
+ 注意:
242
+
243
+ - 私聊 `pairing` 只会把对方加入私聊 allowlist,不会自动给任何群开权限。
244
+ - 群聊权限只看 `groupAllowFrom` 和 `groups.<groupId>.allowFrom`。
245
+
241
246
  如果你要管理“某一个群自己的 `groups.<groupId>.allowFrom` 覆盖”,请用插件工具:
242
247
 
243
248
  - `gewe_manage_group_allowlist`
@@ -260,6 +265,75 @@ GeWe 现在补齐了目录、标准 allowlist 适配和状态摘要。
260
265
  }
261
266
  ```
262
267
 
268
+ ### 认证方式
269
+
270
+ 如果你是第一次用,可以把它理解成两步:
271
+
272
+ 1. 先在私聊里完成配对
273
+ 2. 再在要使用的群里完成认领
274
+
275
+ 为什么要分成两步:
276
+
277
+ - 私聊配对成功,只表示“你可以在私聊里跟机器人说话了”
278
+ - 这一步不会让机器人自动在所有群里生效
279
+ - 群认领是第二次确认,表示“这个群也允许你使用机器人”
280
+ - 这样做是为了避免机器人一进公开群就被任何人直接触发
281
+
282
+ 你实际要做的事很简单:
283
+
284
+ 1. 先和机器人私聊,完成配对
285
+ 2. 再在私聊里让机器人给你一个 8 位认领码
286
+ 3. 把机器人拉进目标群
287
+ 4. 在目标群里直接发这 8 位认领码
288
+ 5. 收到成功提示后,这个群就接入完成了
289
+
290
+ 举个例子:
291
+
292
+ - 机器人在私聊里给你:`MJSQ2G9H`
293
+ - 你把机器人拉进目标群
294
+ - 你在群里直接发:`MJSQ2G9H`
295
+
296
+ 几个容易搞混的点:
297
+
298
+ - 群里不需要 `@机器人`
299
+ - 最推荐的发法就是只发这 8 位码
300
+ - `认领码: MJSQ2G9H` 这种写法也能识别,但默认不推荐
301
+ - 认领码有时效,而且只能用一次
302
+ - 这个码是谁拿到的,就只能由谁来完成这次认领
303
+
304
+ 认领成功后会发生什么:
305
+
306
+ - 机器人会记住“你可以在这个群里使用它了”
307
+ - 只影响当前这个群
308
+ - 不会顺手把别的群也打开
309
+ - 不会自动帮你创建额外的绑定配置
310
+
311
+ 如果你会自己改配置文件:
312
+
313
+ - 认领成功后,插件会把当前发码者写入 `groups.<groupId>.allowFrom`
314
+
315
+ ### 群认领
316
+
317
+ 如果是一个还没放行的新群,推荐流程是:
318
+
319
+ 1. 先在已配对的私聊里调用 `gewe_issue_group_claim_code`
320
+ 2. 把机器人拉进目标群
321
+ 3. 在目标群里只发送这 8 位认领码:`XXXXXXXX`
322
+
323
+ 推荐默认话术:
324
+
325
+ - 私聊里直接把 8 位码发给用户,不要包装成 `认领码: XXXXXXXX`
326
+ - 群里也只发这 8 位码,不要加 `认领码:` 前缀
327
+
328
+ 认领成功后,插件会把“当前发码者”写入该群的 `groups.<groupId>.allowFrom`。
329
+
330
+ 默认特性:
331
+
332
+ - 认领码短时有效、单次使用
333
+ - 只能由签发它的那个发码者在群里使用
334
+ - 只给当前群开权限,不会改顶层 `groupAllowFrom`
335
+ - 不会自动创建 `bindings[]`
336
+
263
337
  如果你就在目标群里调用,`groupId` 可以省略;工具会自动用当前群。
264
338
 
265
339
  ### 状态
package/index.ts CHANGED
@@ -4,6 +4,7 @@ import { createGeweApiTools } from "./src/api-tools.js";
4
4
  import { gewePlugin } from "./src/channel.js";
5
5
  import { createGeweManageGroupAllowlistTool } from "./src/group-allowlist-tool.js";
6
6
  import { createGeweSyncGroupBindingTool } from "./src/group-binding-tool.js";
7
+ import { createGeweIssueGroupClaimCodeTool } from "./src/group-claim-tool.js";
7
8
  import { setGeweRuntime } from "./src/runtime.js";
8
9
 
9
10
  function emptyPluginConfigSchema() {
@@ -44,6 +45,7 @@ const plugin = {
44
45
  api.registerChannel({ plugin: gewePlugin });
45
46
  api.registerTool((ctx) => createGeweApiTools(ctx));
46
47
  api.registerTool((ctx) => createGeweSyncGroupBindingTool(ctx));
48
+ api.registerTool((ctx) => createGeweIssueGroupClaimCodeTool(ctx));
47
49
  api.registerTool((ctx) =>
48
50
  createGeweManageGroupAllowlistTool(ctx, {
49
51
  readConfig: () => api.runtime.config.loadConfig() as never,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gewe-openclaw",
3
- "version": "2026.3.25",
3
+ "version": "2026.3.26",
4
4
  "type": "module",
5
5
  "description": "OpenClaw GeWe channel plugin",
6
6
  "license": "MIT",
@@ -14,12 +14,13 @@ metadata:
14
14
 
15
15
  # GeWe Agent Tools
16
16
 
17
- 优先把这四个工具当成 GeWe 的正式操作面:
17
+ 优先把这五个工具当成 GeWe 的正式操作面:
18
18
 
19
19
  - `gewe_contacts`
20
20
  - `gewe_groups`
21
21
  - `gewe_moments`
22
22
  - `gewe_personal`
23
+ - `gewe_issue_group_claim_code`
23
24
 
24
25
  ## 什么时候用哪个
25
26
 
@@ -27,10 +28,48 @@ metadata:
27
28
  - 处理群资料、群成员、群公告、群管理:用 `gewe_groups`
28
29
  - 处理朋友圈浏览、点赞、评论、发布、转发:用 `gewe_moments`
29
30
  - 处理当前登录微信自己的资料、二维码、安全信息、隐私设置:用 `gewe_personal`
31
+ - 处理新群接入、给当前已配对私聊签发群认领码:用 `gewe_issue_group_claim_code`
30
32
 
31
33
  如果只是想知道“现在这个私聊对象是谁”,优先用 `gewe_contacts`。
32
34
  如果只是想知道“现在这个群是谁、有哪些成员”,优先用 `gewe_groups`。
33
35
 
36
+ ## 新群接入
37
+
38
+ 当用户在 GeWe 当前私聊会话里表达这些意思时,优先进入“群认领”流程,而不是旧的手工绑群流程:
39
+
40
+ - “我要新绑定一个群”
41
+ - “给这个新群开权限”
42
+ - “把机器人接入一个新群”
43
+
44
+ 这时优先调用 `gewe_issue_group_claim_code`。
45
+
46
+ 关键原则:
47
+
48
+ - 不要先让用户手填群 id
49
+ - 不要先让用户自己改配置
50
+ - 不要把“私聊已经 pairing 成功”理解成“群已经自动放行”
51
+
52
+ 标准动作:
53
+
54
+ 1. 在当前私聊会话里调用 `gewe_issue_group_claim_code`
55
+ 2. 把返回的 8 位认领码原样发给用户
56
+ 3. 明确告诉用户:先把机器人拉进目标新群
57
+ 4. 再让用户在目标群里只发送这 8 位认领码,例如:`XXXXXXXX`
58
+
59
+ 重要话术约束:
60
+
61
+ - 默认只把 8 位码发给用户,不要包装成 `认领码: XXXXXXXX`
62
+ - 默认明确提醒:群里也只发这 8 位码,不要加 `认领码:` 前缀
63
+
64
+ 推荐话术要点:
65
+
66
+ - 这是短时有效、单次使用的认领码
67
+ - 认领成功后,只会授权当前发码者在这个新群里触发机器人
68
+ - 如果只是想查新群资料,再在当前群会话里用 `gewe_groups`
69
+
70
+ 只有在用户已经明确处在目标群里,且需求是“查当前群信息 / 查成员 / 查群 id”时,才优先使用 `gewe_groups`。
71
+ 如果需求是“接入一个还没放行的新群”,先发认领码,再谈群信息。
72
+
34
73
  ## 当前会话推断
35
74
 
36
75
  有些 action 可以少填参数,优先利用当前会话:
package/src/api-tools.ts CHANGED
@@ -65,7 +65,7 @@ import {
65
65
  uploadSnsVideoGewe,
66
66
  } from "./moments-api.js";
67
67
  import { normalizeGeweMessagingTarget } from "./normalize.js";
68
- import { normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
68
+ import { buildJsonSchema, normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
69
69
  import {
70
70
  getProfileGewe,
71
71
  getQrCodeGewe,
@@ -74,6 +74,7 @@ import {
74
74
  updateHeadImgGewe,
75
75
  updateProfileGewe,
76
76
  } from "./personal-api.js";
77
+ import { shouldExposeGeweAgentTool } from "./tool-visibility.js";
77
78
  import type { CoreConfig, ResolvedGeweAccount } from "./types.js";
78
79
 
79
80
  const ContactsActionSchema = z.enum([
@@ -243,6 +244,11 @@ const PersonalToolSchema = z
243
244
  })
244
245
  .strict();
245
246
 
247
+ const ContactsToolParameters = buildJsonSchema(ContactsToolSchema) ?? { type: "object" };
248
+ const GroupsToolParameters = buildJsonSchema(GroupsToolSchema) ?? { type: "object" };
249
+ const MomentsToolParameters = buildJsonSchema(MomentsToolSchema) ?? { type: "object" };
250
+ const PersonalToolParameters = buildJsonSchema(PersonalToolSchema) ?? { type: "object" };
251
+
246
252
  type ContactsToolParams = z.infer<typeof ContactsToolSchema>;
247
253
  type GroupsToolParams = z.infer<typeof GroupsToolSchema>;
248
254
  type MomentsToolParams = z.infer<typeof MomentsToolSchema>;
@@ -1150,15 +1156,17 @@ async function executePersonalTool(
1150
1156
  }
1151
1157
  }
1152
1158
 
1153
- export function createGeweApiTools(ctx: OpenClawPluginToolContext): AnyAgentTool[] {
1159
+ export function createGeweApiTools(ctx: OpenClawPluginToolContext): AnyAgentTool[] | null {
1160
+ if (!shouldExposeGeweAgentTool(ctx)) {
1161
+ return null;
1162
+ }
1154
1163
  return [
1155
1164
  {
1156
1165
  name: "gewe_contacts",
1157
1166
  label: "GeWe Contacts",
1158
1167
  description:
1159
1168
  "GeWe contacts operations. Actions: list, list_cache, brief, detail, search, search_im, im_detail, check_relation, set_remark, set_only_chat, delete, add, add_im, sync_im, phones_get, phones_upload.",
1160
- ownerOnly: true,
1161
- parameters: ContactsToolSchema,
1169
+ parameters: ContactsToolParameters,
1162
1170
  execute: async (_toolCallId, rawParams) => {
1163
1171
  const params = ContactsToolSchema.parse(rawParams ?? {});
1164
1172
  try {
@@ -1184,8 +1192,7 @@ export function createGeweApiTools(ctx: OpenClawPluginToolContext): AnyAgentTool
1184
1192
  label: "GeWe Groups",
1185
1193
  description:
1186
1194
  "GeWe group operations. Actions: info, announcement, members, member_detail, qr_code, set_self_nickname, rename, set_remark, create, remove_members, agree_join, join_via_qr, add_member_as_friend, approve_join_request, admin_operate, save_to_contacts, pin, disband, set_silence, set_announcement, quit, invite.",
1187
- ownerOnly: true,
1188
- parameters: GroupsToolSchema,
1195
+ parameters: GroupsToolParameters,
1189
1196
  execute: async (_toolCallId, rawParams) => {
1190
1197
  const params = GroupsToolSchema.parse(rawParams ?? {});
1191
1198
  try {
@@ -1211,8 +1218,7 @@ export function createGeweApiTools(ctx: OpenClawPluginToolContext): AnyAgentTool
1211
1218
  label: "GeWe Moments",
1212
1219
  description:
1213
1220
  "GeWe Moments operations. Actions: list_self, list_contact, detail, download_video, upload_image, upload_video, delete, post_text, post_image, post_video, post_link, set_stranger_visibility, set_visible_scope, set_privacy, like, comment, forward.",
1214
- ownerOnly: true,
1215
- parameters: MomentsToolSchema,
1221
+ parameters: MomentsToolParameters,
1216
1222
  execute: async (_toolCallId, rawParams) => {
1217
1223
  const params = MomentsToolSchema.parse(rawParams ?? {});
1218
1224
  try {
@@ -1238,8 +1244,7 @@ export function createGeweApiTools(ctx: OpenClawPluginToolContext): AnyAgentTool
1238
1244
  label: "GeWe Personal",
1239
1245
  description:
1240
1246
  "GeWe personal-account operations. Actions: profile, qrcode, safety_info, update_profile, update_avatar, privacy.",
1241
- ownerOnly: true,
1242
- parameters: PersonalToolSchema,
1247
+ parameters: PersonalToolParameters,
1243
1248
  execute: async (_toolCallId, rawParams) => {
1244
1249
  const params = PersonalToolSchema.parse(rawParams ?? {});
1245
1250
  try {
package/src/delivery.ts CHANGED
@@ -770,7 +770,6 @@ function buildPartialQuoteXml(params?: {
770
770
  function buildQuoteReplyAppMsg(params: {
771
771
  title: string;
772
772
  svrid: string;
773
- atWxid?: string;
774
773
  partialText?: {
775
774
  text?: string;
776
775
  start?: string;
@@ -782,12 +781,12 @@ function buildQuoteReplyAppMsg(params: {
782
781
  }): string {
783
782
  const safeTitle = escapeXmlText(params.title.trim() || "引用回复");
784
783
  const safeSvrid = escapeXmlText(params.svrid.trim());
785
- const safeAtWxid = params.atWxid?.trim() ? escapeXmlText(params.atWxid.trim()) : undefined;
786
- const encodedMsgSource = safeAtWxid
787
- ? `&lt;msgsource&gt;&lt;atuserlist&gt;${safeAtWxid}&lt;/atuserlist&gt;&lt;/msgsource&gt;`
788
- : "";
789
784
  const partialTextXml = buildPartialQuoteXml(params.partialText);
790
- return `<appmsg><title>${safeTitle}</title><type>57</type><refermsg>${partialTextXml}<svrid>${safeSvrid}</svrid>${safeAtWxid ? `<msgsource>${encodedMsgSource}</msgsource>` : ""}</refermsg></appmsg>`;
785
+ return `<appmsg><title>${safeTitle}</title><type>57</type><refermsg>${partialTextXml}<svrid>${safeSvrid}</svrid></refermsg></appmsg>`;
786
+ }
787
+
788
+ function summarizeOutboundText(value: string): string {
789
+ return JSON.stringify(value.replace(/\s+/g, " ").trim().slice(0, 120));
791
790
  }
792
791
 
793
792
  async function stageMedia(params: {
@@ -983,15 +982,18 @@ export async function deliverGewePayload(params: {
983
982
  : payload.replyToId?.trim() || "";
984
983
  const quoteReplyTitle = geweData?.quoteReply?.title?.trim() || trimmedText;
985
984
  if (quoteReplySvrid && quoteReplyTitle && geweData?.quoteReply) {
985
+ core.log?.(
986
+ `gewe: outbound quoteReply explicit to=${toWxid} ats=${JSON.stringify(geweData.quoteReply.atWxid?.trim() || "")} title=${summarizeOutboundText(quoteReplyTitle)}`,
987
+ );
986
988
  const result = await sendAppMsgGewe({
987
989
  account,
988
990
  toWxid,
989
991
  appmsg: buildQuoteReplyAppMsg({
990
992
  svrid: quoteReplySvrid,
991
993
  title: quoteReplyTitle,
992
- atWxid: geweData.quoteReply.atWxid?.trim(),
993
994
  partialText: geweData.quoteReply.partialText,
994
995
  }),
996
+ ats: geweData.quoteReply.atWxid?.trim(),
995
997
  });
996
998
  core.channel.activity.record({
997
999
  channel: CHANNEL_ID,
@@ -1160,6 +1162,9 @@ export async function deliverGewePayload(params: {
1160
1162
  }
1161
1163
 
1162
1164
  if (autoQuoteReplyEnabled && trimmedText && payload.replyToId?.trim() && !mediaUrl) {
1165
+ core.log?.(
1166
+ `gewe: outbound quoteReply auto to=${toWxid} ats=${JSON.stringify(geweData?.ats?.trim() || "")} title=${summarizeOutboundText(trimmedText)}`,
1167
+ );
1163
1168
  const result = await sendAppMsgGewe({
1164
1169
  account,
1165
1170
  toWxid,
@@ -1168,6 +1173,7 @@ export async function deliverGewePayload(params: {
1168
1173
  title: trimmedText,
1169
1174
  partialText: autoQuoteContext?.partialText,
1170
1175
  }),
1176
+ ats: geweData?.ats,
1171
1177
  });
1172
1178
  core.channel.activity.record({
1173
1179
  channel: CHANNEL_ID,
@@ -4,8 +4,8 @@ import { z } from "zod";
4
4
  import { resolveGeweAccount } from "./accounts.js";
5
5
  import { ensureGeweWriteSection } from "./config-edit.js";
6
6
  import { normalizeGeweBindingConversationId, inferCurrentGeweGroupId } from "./group-binding.js";
7
- import { normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
8
- import { readGeweAllowFromStore } from "./pairing-store.js";
7
+ import { buildJsonSchema, normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
8
+ import { shouldExposeGeweAgentTool } from "./tool-visibility.js";
9
9
  import type { GeweGroupConfig } from "./types.js";
10
10
 
11
11
  const GeweManageGroupAllowlistSchema = z
@@ -17,6 +17,9 @@ const GeweManageGroupAllowlistSchema = z
17
17
  })
18
18
  .strict();
19
19
 
20
+ const GeweManageGroupAllowlistParameters =
21
+ buildJsonSchema(GeweManageGroupAllowlistSchema) ?? { type: "object" };
22
+
20
23
  function jsonResult(details: Record<string, unknown>) {
21
24
  return {
22
25
  content: [{ type: "text" as const, text: JSON.stringify(details, null, 2) }],
@@ -80,9 +83,7 @@ function readOverrideEntries(accountConfig: Record<string, unknown>, groupId: st
80
83
 
81
84
  function resolveEffectiveEntries(params: {
82
85
  accountConfig: Record<string, unknown>;
83
- accountId: string;
84
86
  groupId: string;
85
- pairingEntries: string[];
86
87
  }): {
87
88
  baseEntries: string[];
88
89
  overrideEntries: string[];
@@ -93,7 +94,7 @@ function resolveEffectiveEntries(params: {
93
94
  return {
94
95
  baseEntries,
95
96
  overrideEntries,
96
- effectiveEntries: dedupeEntries([...baseEntries, ...params.pairingEntries, ...overrideEntries]),
97
+ effectiveEntries: dedupeEntries([...baseEntries, ...overrideEntries]),
97
98
  };
98
99
  }
99
100
 
@@ -160,14 +161,16 @@ export function createGeweManageGroupAllowlistTool(
160
161
  readConfig?: () => OpenClawConfig;
161
162
  writeConfigFile?: (next: OpenClawConfig) => Promise<void>;
162
163
  },
163
- ): AnyAgentTool {
164
+ ): AnyAgentTool | null {
165
+ if (!shouldExposeGeweAgentTool(ctx)) {
166
+ return null;
167
+ }
164
168
  return {
165
169
  name: "gewe_manage_group_allowlist",
166
170
  label: "GeWe Manage Group Allowlist",
167
171
  description:
168
172
  "Inspect or edit a GeWe group's allowFrom override. Modes: inspect, add, remove, replace, clear.",
169
- ownerOnly: true,
170
- parameters: GeweManageGroupAllowlistSchema,
173
+ parameters: GeweManageGroupAllowlistParameters,
171
174
  execute: async (_toolCallId, rawParams) => {
172
175
  const params = GeweManageGroupAllowlistSchema.parse(rawParams ?? {});
173
176
  const cfg = resolveToolConfig(ctx, deps?.readConfig);
@@ -189,12 +192,9 @@ export function createGeweManageGroupAllowlistTool(
189
192
  ?.accounts?.[accountId] ?? {})
190
193
  ) as Record<string, unknown>;
191
194
 
192
- const pairingEntries = await readGeweAllowFromStore({ accountId });
193
195
  const current = resolveEffectiveEntries({
194
196
  accountConfig: resolvedAccount.config as Record<string, unknown>,
195
- accountId,
196
197
  groupId,
197
- pairingEntries,
198
198
  });
199
199
 
200
200
  if (params.mode === "inspect") {
@@ -205,7 +205,6 @@ export function createGeweManageGroupAllowlistTool(
205
205
  groupId,
206
206
  groupPolicy: resolvedAccount.config.groupPolicy ?? "allowlist",
207
207
  baseEntries: current.baseEntries,
208
- pairingEntries,
209
208
  overrideEntries: current.overrideEntries,
210
209
  effectiveEntries: current.effectiveEntries,
211
210
  });
@@ -15,7 +15,8 @@ import {
15
15
  resolveGeweBindingIdentityConfigForGroup,
16
16
  resolveGeweCurrentSelfNickname,
17
17
  } from "./group-binding.js";
18
- import { normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
18
+ import { buildJsonSchema, normalizeAccountId, type OpenClawConfig } from "./openclaw-compat.js";
19
+ import { shouldExposeGeweAgentTool } from "./tool-visibility.js";
19
20
 
20
21
  const GeweSyncGroupBindingToolSchema = z
21
22
  .object({
@@ -27,6 +28,9 @@ const GeweSyncGroupBindingToolSchema = z
27
28
  })
28
29
  .strict();
29
30
 
31
+ const GeweSyncGroupBindingToolParameters =
32
+ buildJsonSchema(GeweSyncGroupBindingToolSchema) ?? { type: "object" };
33
+
30
34
  function jsonResult(details: Record<string, unknown>) {
31
35
  return {
32
36
  content: [{ type: "text" as const, text: JSON.stringify(details, null, 2) }],
@@ -34,14 +38,16 @@ function jsonResult(details: Record<string, unknown>) {
34
38
  };
35
39
  }
36
40
 
37
- export function createGeweSyncGroupBindingTool(ctx: OpenClawPluginToolContext): AnyAgentTool {
41
+ export function createGeweSyncGroupBindingTool(ctx: OpenClawPluginToolContext): AnyAgentTool | null {
42
+ if (!shouldExposeGeweAgentTool(ctx)) {
43
+ return null;
44
+ }
38
45
  return {
39
46
  name: "gewe_sync_group_binding",
40
47
  label: "GeWe Sync Group Binding",
41
48
  description:
42
49
  "Inspect or manually sync a GeWe group binding identity. Modes: inspect, dry_run, apply.",
43
- ownerOnly: true,
44
- parameters: GeweSyncGroupBindingToolSchema,
50
+ parameters: GeweSyncGroupBindingToolParameters,
45
51
  execute: async (_toolCallId, rawParams) => {
46
52
  const params = GeweSyncGroupBindingToolSchema.parse(rawParams ?? {});
47
53
  const cfg = (ctx.config ?? {}) as OpenClawConfig;
@@ -0,0 +1,103 @@
1
+ import type { AnyAgentTool, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
2
+ import { z } from "zod";
3
+
4
+ import { normalizeGeweMessagingTarget } from "./normalize.js";
5
+ import { buildJsonSchema, normalizeAccountId } from "./openclaw-compat.js";
6
+ import { issueGeweGroupClaimCode } from "./pairing-store.js";
7
+ import { shouldExposeGeweAgentTool } from "./tool-visibility.js";
8
+
9
+ const GeweIssueGroupClaimCodeSchema = z
10
+ .object({
11
+ accountId: z.string().optional(),
12
+ })
13
+ .strict();
14
+
15
+ const GeweIssueGroupClaimCodeParameters =
16
+ buildJsonSchema(GeweIssueGroupClaimCodeSchema) ?? { type: "object" };
17
+
18
+ function extractSessionScopedTarget(
19
+ sessionKey: string | undefined,
20
+ markers: string[],
21
+ ): string | undefined {
22
+ const raw = sessionKey?.trim();
23
+ if (!raw) {
24
+ return undefined;
25
+ }
26
+ const lowered = raw.toLowerCase();
27
+ for (const marker of markers) {
28
+ const index = lowered.indexOf(marker);
29
+ if (index === -1) {
30
+ continue;
31
+ }
32
+ const value = raw.slice(index + marker.length).trim();
33
+ if (value) {
34
+ return value;
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+
40
+ function inferCurrentGeweDirectWxid(ctx: OpenClawPluginToolContext): string | undefined {
41
+ const fromSession = extractSessionScopedTarget(ctx.sessionKey, [
42
+ ":gewe-openclaw:direct:",
43
+ ":gewe-openclaw:dm:",
44
+ ":gewe:direct:",
45
+ ":gewe:dm:",
46
+ ]);
47
+ const normalizedSession = normalizeGeweMessagingTarget(fromSession ?? "");
48
+ if (normalizedSession && !normalizedSession.endsWith("@chatroom")) {
49
+ return normalizedSession;
50
+ }
51
+
52
+ const normalizedRequester = normalizeGeweMessagingTarget(ctx.requesterSenderId ?? "");
53
+ if (normalizedRequester && !normalizedRequester.endsWith("@chatroom")) {
54
+ return normalizedRequester;
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ function jsonResult(details: Record<string, unknown>) {
60
+ return {
61
+ content: [{ type: "text" as const, text: JSON.stringify(details, null, 2) }],
62
+ details,
63
+ };
64
+ }
65
+
66
+ export function createGeweIssueGroupClaimCodeTool(ctx: OpenClawPluginToolContext): AnyAgentTool | null {
67
+ if (!shouldExposeGeweAgentTool(ctx)) {
68
+ return null;
69
+ }
70
+ return {
71
+ name: "gewe_issue_group_claim_code",
72
+ label: "GeWe Issue Group Claim Code",
73
+ description:
74
+ "Issue a short-lived single-use group claim code for the current GeWe direct-message session.",
75
+ parameters: GeweIssueGroupClaimCodeParameters,
76
+ execute: async (_toolCallId, rawParams) => {
77
+ const params = GeweIssueGroupClaimCodeSchema.parse(rawParams ?? {});
78
+ const accountId = normalizeAccountId(params.accountId ?? ctx.agentAccountId ?? "default");
79
+ const issuerId = inferCurrentGeweDirectWxid(ctx);
80
+ if (!issuerId) {
81
+ throw new Error(
82
+ "GeWe group claim code issuance requires a current GeWe direct-message session to infer the owner wxid.",
83
+ );
84
+ }
85
+
86
+ const issued = await issueGeweGroupClaimCode({
87
+ accountId,
88
+ issuerId,
89
+ });
90
+
91
+ return jsonResult({
92
+ ok: true,
93
+ accountId: issued.accountId,
94
+ issuerId: issued.issuerId,
95
+ code: issued.code,
96
+ recommendedGroupMessage: issued.code,
97
+ createdAt: issued.createdAt,
98
+ expiresAt: issued.expiresAt,
99
+ usageHint: `把机器人拉进目标群后,在群里只发送这 8 位认领码:${issued.code}(不要加“认领码:”前缀)`,
100
+ });
101
+ },
102
+ };
103
+ }
package/src/inbound.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  buildGeweInboundMediaPayload,
15
15
  buildGeweInboundMessageMeta,
16
16
  } from "./inbound-batch.js";
17
+ import { ensureGeweWriteSection } from "./config-edit.js";
17
18
  import type { GeweDownloadQueue } from "./download-queue.js";
18
19
  import { downloadGeweFile, downloadGeweImage, downloadGeweVideo, downloadGeweVoice } from "./download.js";
19
20
  import { deliverGewePayload } from "./delivery.js";
@@ -21,7 +22,11 @@ import { applyGeweReplyModeToPayload, resolveGeweReplyOptions } from "./reply-op
21
22
  import { rememberGeweDirectoryObservation } from "./directory-cache.js";
22
23
  import { getGeweRuntime } from "./runtime.js";
23
24
  import { ensureRustSilkBinary } from "./silk.js";
24
- import { readGeweAllowFromStore, redeemGewePairCode } from "./pairing-store.js";
25
+ import {
26
+ readGeweAllowFromStore,
27
+ redeemGeweGroupClaimCode,
28
+ redeemGewePairCode,
29
+ } from "./pairing-store.js";
25
30
  import {
26
31
  normalizeGeweAllowlist,
27
32
  resolveGeweAllowlistMatch,
@@ -100,6 +105,12 @@ const GEWE_PAIR_CODE_REGEX = /^[A-HJ-NP-Z2-9]{8}$/i;
100
105
  const GEWE_PAIR_CODE_PREFIX_REGEX = /^配对码\s*[::]?\s*([A-HJ-NP-Z2-9]{8})$/i;
101
106
  const GEWE_PAIR_CODE_SUCCESS_REPLY = "配对成功,已加入允许列表。请重新发送上一条消息。";
102
107
  const GEWE_PAIR_CODE_INVALID_REPLY = "配对码无效或已过期。";
108
+ const GEWE_GROUP_CLAIM_CODE_PREFIX_REGEX = /^(?:群)?认领码\s*[::]?\s*([A-HJ-NP-Z2-9]{8})$/i;
109
+ const GEWE_GROUP_CLAIM_CODE_INLINE_REGEX = /(?:^|\s)(?:群)?认领码\s*[::]?\s*([A-HJ-NP-Z2-9]{8})(?=$|\s)/i;
110
+ const GEWE_GROUP_CLAIM_CODE_SUCCESS_REPLY =
111
+ "当前群认领成功,已授权你在本群触发机器人。请重新发送上一条消息。";
112
+ const GEWE_GROUP_CLAIM_CODE_INVALID_REPLY = "认领码无效、已过期,或不属于当前发送者。";
113
+ const GEWE_GROUP_CLAIM_CODE_DISABLED_REPLY = "当前群已被显式禁用,无法认领。";
103
114
 
104
115
  function resolveMediaPlaceholder(msgType: number): string {
105
116
  if (msgType === 3) return "<media:image>";
@@ -158,6 +169,63 @@ function resolveGewePairCodeCandidate(rawBody: string): string | null {
158
169
  return prefixed?.[1]?.toUpperCase() ?? null;
159
170
  }
160
171
 
172
+ function resolveGeweGroupClaimCodeCandidate(rawBody: string): string | null {
173
+ const trimmed = rawBody.trim();
174
+ if (!trimmed) return null;
175
+ if (GEWE_PAIR_CODE_REGEX.test(trimmed)) {
176
+ return trimmed.toUpperCase();
177
+ }
178
+
179
+ const candidateLines = trimmed
180
+ .split(/\r?\n/)
181
+ .map((line) => line.trim())
182
+ .filter(Boolean)
183
+ .map((line) => {
184
+ let next = line;
185
+ while (next.startsWith("@")) {
186
+ next = next.replace(/^@\S+\s*/u, "").trim();
187
+ }
188
+ return next;
189
+ });
190
+
191
+ for (const line of candidateLines) {
192
+ if (!line) {
193
+ continue;
194
+ }
195
+ if (GEWE_PAIR_CODE_REGEX.test(line)) {
196
+ return line.toUpperCase();
197
+ }
198
+ const prefixed = line.match(GEWE_GROUP_CLAIM_CODE_PREFIX_REGEX);
199
+ if (prefixed?.[1]) {
200
+ return prefixed[1].toUpperCase();
201
+ }
202
+ }
203
+
204
+ const inline = trimmed.match(GEWE_GROUP_CLAIM_CODE_INLINE_REGEX);
205
+ return inline?.[1]?.toUpperCase() ?? null;
206
+ }
207
+
208
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
209
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
210
+ return undefined;
211
+ }
212
+ return value as Record<string, unknown>;
213
+ }
214
+
215
+ function dedupeAllowEntries(values: readonly unknown[]): string[] {
216
+ const result: string[] = [];
217
+ const seen = new Set<string>();
218
+ for (const value of values) {
219
+ const normalized = String(value ?? "").trim().replace(/^(?:gewe|wechat|wx):/i, "");
220
+ if (!normalized || seen.has(normalized)) {
221
+ continue;
222
+ }
223
+ seen.add(normalized);
224
+ result.push(normalized);
225
+ }
226
+ return result;
227
+ }
228
+
161
229
  function looksLikeSilkVoice(params: {
162
230
  buffer: Buffer;
163
231
  contentType?: string | null;
@@ -727,6 +795,16 @@ export async function handleGeweInboundBatch(params: {
727
795
  senderName,
728
796
  groupId,
729
797
  });
798
+ const nativeAtWxids = Array.from(
799
+ new Set(
800
+ entries
801
+ .flatMap((entry) => entry.message.atWxids ?? [])
802
+ .map((wxid) => wxid.trim())
803
+ .filter(Boolean),
804
+ ),
805
+ );
806
+ const nativeAtAll = entries.some((entry) => entry.message.atAll === true);
807
+ const nativeAtTriggered = nativeAtWxids.includes(lastMessage.botWxid.trim());
730
808
 
731
809
  const dmPolicy = account.config.dmPolicy ?? "pairing";
732
810
  const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
@@ -759,6 +837,78 @@ export async function handleGeweInboundBatch(params: {
759
837
  })
760
838
  : undefined;
761
839
 
840
+ if (isGroup) {
841
+ const groupClaimCode = resolveGeweGroupClaimCodeCandidate(rawBodyCandidate);
842
+ if (groupClaimCode) {
843
+ const latestCfg = (core.config.loadConfig?.() as CoreConfig | undefined) ?? config;
844
+ const writeConfigFile = core.config.writeConfigFile?.bind(core.config);
845
+ const write =
846
+ groupId && writeConfigFile
847
+ ? ensureGeweWriteSection({
848
+ cfg: latestCfg as OpenClawConfig,
849
+ accountId: account.accountId,
850
+ })
851
+ : null;
852
+ const groups =
853
+ write && asRecord(write.target.groups)
854
+ ? (asRecord(write.target.groups) as Record<string, Record<string, unknown>>)
855
+ : ({} as Record<string, Record<string, unknown>>);
856
+ const currentGroup = groupId ? asRecord(groups[groupId]) ?? {} : {};
857
+ const isDisabled =
858
+ groupPolicy === "disabled" ||
859
+ groupMatch?.groupConfig?.enabled === false ||
860
+ groupMatch?.wildcardConfig?.enabled === false ||
861
+ currentGroup.enabled === false;
862
+ const claimReply =
863
+ !groupId || !write || !writeConfigFile
864
+ ? GEWE_GROUP_CLAIM_CODE_INVALID_REPLY
865
+ : isDisabled
866
+ ? GEWE_GROUP_CLAIM_CODE_DISABLED_REPLY
867
+ : await (async () => {
868
+ const redeemed = await redeemGeweGroupClaimCode({
869
+ accountId: account.accountId,
870
+ code: groupClaimCode,
871
+ issuerId: senderId,
872
+ groupId,
873
+ }).catch((err) => {
874
+ runtime.error?.(
875
+ `gewe: group claim code redeem failed for ${senderId} in ${groupId}: ${String(err)}`,
876
+ );
877
+ return null;
878
+ });
879
+ if (!redeemed) {
880
+ return GEWE_GROUP_CLAIM_CODE_INVALID_REPLY;
881
+ }
882
+ write.target.groups = groups;
883
+ const allowFrom = dedupeAllowEntries([
884
+ ...(Array.isArray(currentGroup.allowFrom) ? currentGroup.allowFrom : []),
885
+ senderId,
886
+ ]);
887
+ groups[groupId] = {
888
+ ...currentGroup,
889
+ allowFrom,
890
+ };
891
+ await writeConfigFile(write.nextCfg);
892
+ return GEWE_GROUP_CLAIM_CODE_SUCCESS_REPLY;
893
+ })();
894
+
895
+ try {
896
+ await deliverGewePayload({
897
+ payload: {
898
+ text: claimReply,
899
+ },
900
+ account,
901
+ cfg: config as OpenClawConfig,
902
+ toWxid,
903
+ statusSink: (patch) => statusSink?.(patch),
904
+ });
905
+ } catch (err) {
906
+ runtime.error?.(`gewe: group claim code reply failed for ${senderId}: ${String(err)}`);
907
+ }
908
+ return;
909
+ }
910
+ }
911
+
762
912
  if (isGroup && groupMatch && !groupMatch.allowed) {
763
913
  runtime.log?.(`gewe: drop group ${groupId} (not allowlisted)`);
764
914
  return;
@@ -772,10 +922,9 @@ export async function handleGeweInboundBatch(params: {
772
922
  const wildcardRoomAllowFrom = normalizeGeweAllowlist(groupMatch?.wildcardConfig?.allowFrom);
773
923
  const roomAllowFrom =
774
924
  directRoomAllowFrom.length > 0 ? directRoomAllowFrom : wildcardRoomAllowFrom;
775
- const baseGroupAllowFrom =
776
- configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom;
925
+ const baseGroupAllowFrom = configGroupAllowFrom;
777
926
  const effectiveAllowFrom = [...configAllowFrom, ...storeAllowList].filter(Boolean);
778
- const effectiveGroupAllowFrom = [...baseGroupAllowFrom, ...storeAllowList].filter(Boolean);
927
+ const effectiveGroupAllowFrom = [...baseGroupAllowFrom].filter(Boolean);
779
928
 
780
929
  const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
781
930
  cfg: config as OpenClawConfig,
@@ -883,16 +1032,6 @@ export async function handleGeweInboundBatch(params: {
883
1032
  config as OpenClawConfig,
884
1033
  route.agentId,
885
1034
  );
886
- const nativeAtWxids = Array.from(
887
- new Set(
888
- entries
889
- .flatMap((entry) => entry.message.atWxids ?? [])
890
- .map((wxid) => wxid.trim())
891
- .filter(Boolean),
892
- ),
893
- );
894
- const nativeAtAll = entries.some((entry) => entry.message.atAll === true);
895
- const nativeAtTriggered = nativeAtWxids.includes(lastMessage.botWxid.trim());
896
1035
  const regexAtTriggered = mentionRegexes.length
897
1036
  ? core.channel.mentions.matchesMentionPatterns(rawBodyCandidate, mentionRegexes)
898
1037
  : false;
@@ -567,13 +567,10 @@ export function logInboundDrop(params: {
567
567
  }
568
568
 
569
569
  export function buildChannelConfigSchema(schema: ZodTypeAny): ChannelConfigSchema {
570
- const schemaWithJson = schema as ZodSchemaWithToJsonSchema;
571
- if (typeof schemaWithJson.toJSONSchema === "function") {
570
+ const jsonSchema = buildJsonSchema(schema);
571
+ if (jsonSchema) {
572
572
  return {
573
- schema: schemaWithJson.toJSONSchema({
574
- target: "draft-07",
575
- unrepresentable: "any",
576
- }) as Record<string, unknown>,
573
+ schema: jsonSchema,
577
574
  };
578
575
  }
579
576
  return {
@@ -584,6 +581,20 @@ export function buildChannelConfigSchema(schema: ZodTypeAny): ChannelConfigSchem
584
581
  };
585
582
  }
586
583
 
584
+ export function buildJsonSchema(schema: unknown): Record<string, unknown> | null {
585
+ const schemaWithJson = schema as ZodSchemaWithToJsonSchema | null | undefined;
586
+ if (schemaWithJson && typeof schemaWithJson.toJSONSchema === "function") {
587
+ return schemaWithJson.toJSONSchema({
588
+ target: "draft-07",
589
+ unrepresentable: "any",
590
+ }) as Record<string, unknown>;
591
+ }
592
+ if (schema && typeof schema === "object" && !Array.isArray(schema)) {
593
+ return schema as Record<string, unknown>;
594
+ }
595
+ return null;
596
+ }
597
+
587
598
  export const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
588
599
  export const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]);
589
600
 
@@ -7,9 +7,11 @@ import { CHANNEL_ID, stripChannelPrefix } from "./constants.js";
7
7
  import { resolveOpenClawStateDir } from "./state-paths.js";
8
8
 
9
9
  const PAIR_CODE_TTL_MS = 60 * 60 * 1000;
10
+ const GROUP_CLAIM_CODE_TTL_MS = 5 * 60 * 1000;
10
11
  const LOCK_STALE_MS = 30_000;
11
12
  const LOCK_RETRY_ATTEMPTS = 12;
12
13
  const LOCK_RETRY_BASE_MS = 50;
14
+ const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
13
15
 
14
16
  type AllowFromStore = {
15
17
  version: 1;
@@ -27,6 +29,20 @@ type GewePairCodeStore = {
27
29
  codes: Array<string | GewePairCodeEntry>;
28
30
  };
29
31
 
32
+ type GeweGroupClaimCodeEntry = {
33
+ code: string;
34
+ accountId?: string;
35
+ issuerId?: string;
36
+ createdAt?: string;
37
+ usedAt?: string;
38
+ usedGroupId?: string;
39
+ };
40
+
41
+ type GeweGroupClaimCodeStore = {
42
+ version: 1;
43
+ codes: GeweGroupClaimCodeEntry[];
44
+ };
45
+
30
46
  type LegacyPairingRequest = {
31
47
  id?: string;
32
48
  code?: string;
@@ -53,6 +69,15 @@ type CanonicalLegacyPairingRequest = {
53
69
  persisted: LegacyPairingRequest;
54
70
  };
55
71
 
72
+ type CanonicalGroupClaimCodeEntry = {
73
+ code: string;
74
+ accountId: string;
75
+ issuerId: string;
76
+ createdAt?: string;
77
+ usedAt?: string;
78
+ usedGroupId?: string;
79
+ };
80
+
56
81
  function resolveCredentialsDir(env: NodeJS.ProcessEnv = process.env): string {
57
82
  return path.join(resolveOpenClawStateDir(env), "credentials");
58
83
  }
@@ -89,6 +114,11 @@ function normalizePairCode(code: string | undefined | null): string {
89
114
  return (code ?? "").trim().toUpperCase();
90
115
  }
91
116
 
117
+ function normalizeGroupClaimIssuerId(value: string | undefined | null): string {
118
+ const normalized = stripChannelPrefix(String(value ?? "").trim());
119
+ return normalized || "";
120
+ }
121
+
92
122
  function normalizeAllowFromEntry(entry: string | number): string {
93
123
  const normalized = stripChannelPrefix(String(entry).trim());
94
124
  if (!normalized || normalized === "*") {
@@ -122,6 +152,22 @@ function isExpired(createdAt: string | undefined, nowMs: number, allowMissing =
122
152
  return nowMs - parsed > PAIR_CODE_TTL_MS;
123
153
  }
124
154
 
155
+ function isExpiredWithTtl(params: {
156
+ createdAt: string | undefined;
157
+ nowMs: number;
158
+ ttlMs: number;
159
+ allowMissing?: boolean;
160
+ }): boolean {
161
+ if (!params.createdAt) {
162
+ return params.allowMissing !== true;
163
+ }
164
+ const parsed = Date.parse(params.createdAt);
165
+ if (!Number.isFinite(parsed)) {
166
+ return true;
167
+ }
168
+ return params.nowMs - parsed > params.ttlMs;
169
+ }
170
+
125
171
  async function readJsonFileWithFallback<T>(
126
172
  filePath: string,
127
173
  fallback: T,
@@ -277,6 +323,50 @@ function persistPairCodeEntry(entry: CanonicalPairCodeEntry): GewePairCodeEntry
277
323
  };
278
324
  }
279
325
 
326
+ function canonicalizeGroupClaimCodeEntry(
327
+ raw: GeweGroupClaimCodeEntry,
328
+ ): CanonicalGroupClaimCodeEntry | null {
329
+ if (!raw || typeof raw !== "object") {
330
+ return null;
331
+ }
332
+ const code = normalizePairCode(raw.code);
333
+ const issuerId = normalizeGroupClaimIssuerId(raw.issuerId);
334
+ if (!code || !issuerId) {
335
+ return null;
336
+ }
337
+ return {
338
+ code,
339
+ accountId: normalizeStoreAccountId(raw.accountId),
340
+ issuerId,
341
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : undefined,
342
+ usedAt: typeof raw.usedAt === "string" ? raw.usedAt : undefined,
343
+ usedGroupId:
344
+ typeof raw.usedGroupId === "string" ? stripChannelPrefix(raw.usedGroupId.trim()) : undefined,
345
+ };
346
+ }
347
+
348
+ function persistGroupClaimCodeEntry(
349
+ entry: CanonicalGroupClaimCodeEntry,
350
+ ): GeweGroupClaimCodeEntry {
351
+ return {
352
+ code: entry.code,
353
+ ...(entry.accountId !== DEFAULT_ACCOUNT_ID ? { accountId: entry.accountId } : {}),
354
+ issuerId: entry.issuerId,
355
+ ...(entry.createdAt ? { createdAt: entry.createdAt } : {}),
356
+ ...(entry.usedAt ? { usedAt: entry.usedAt } : {}),
357
+ ...(entry.usedGroupId ? { usedGroupId: entry.usedGroupId } : {}),
358
+ };
359
+ }
360
+
361
+ function randomCode(length: number): string {
362
+ const bytes = crypto.randomBytes(length);
363
+ let output = "";
364
+ for (let index = 0; index < length; index += 1) {
365
+ output += CODE_ALPHABET[bytes[index] % CODE_ALPHABET.length];
366
+ }
367
+ return output;
368
+ }
369
+
280
370
  function canonicalizeLegacyPairingRequest(
281
371
  raw: LegacyPairingRequest,
282
372
  ): CanonicalLegacyPairingRequest | null {
@@ -452,6 +542,16 @@ export function resolveGeweLegacyPairingPath(
452
542
  return path.join(resolveCredentialsDir(env), `${safeChannelKey(CHANNEL_ID)}-pairing.json`);
453
543
  }
454
544
 
545
+ export function resolveGeweGroupClaimCodesPath(
546
+ accountId?: string,
547
+ env: NodeJS.ProcessEnv = process.env,
548
+ ): string {
549
+ return path.join(
550
+ resolveCredentialsDir(env),
551
+ `${safeChannelKey(CHANNEL_ID)}-${safeAccountKey(normalizeStoreAccountId(accountId))}-group-claim-codes.json`,
552
+ );
553
+ }
554
+
455
555
  export async function readGeweAllowFromStore(params: {
456
556
  accountId?: string;
457
557
  env?: NodeJS.ProcessEnv;
@@ -476,3 +576,184 @@ export async function redeemGewePairCode(params: {
476
576
  (await redeemFromLegacyPairingStore(params))
477
577
  );
478
578
  }
579
+
580
+ export async function issueGeweGroupClaimCode(params: {
581
+ accountId?: string;
582
+ issuerId: string;
583
+ env?: NodeJS.ProcessEnv;
584
+ }): Promise<{
585
+ code: string;
586
+ accountId: string;
587
+ issuerId: string;
588
+ createdAt: string;
589
+ expiresAt: string;
590
+ }> {
591
+ const resolvedAccountId = normalizeStoreAccountId(params.accountId);
592
+ const issuerId = normalizeGroupClaimIssuerId(params.issuerId);
593
+ if (!issuerId) {
594
+ throw new Error("invalid GeWe group claim issuer");
595
+ }
596
+ const filePath = resolveGeweGroupClaimCodesPath(resolvedAccountId, params.env);
597
+ const createdAt = new Date().toISOString();
598
+ const expiresAt = new Date(Date.parse(createdAt) + GROUP_CLAIM_CODE_TTL_MS).toISOString();
599
+
600
+ const code = await withFileLock(filePath, async () => {
601
+ const { value } = await readJsonFileWithFallback<GeweGroupClaimCodeStore>(filePath, {
602
+ version: 1,
603
+ codes: [],
604
+ });
605
+ const nowMs = Date.now();
606
+ const codes = Array.isArray(value.codes) ? value.codes : [];
607
+ const nextCodes: GeweGroupClaimCodeEntry[] = [];
608
+ const activeCodes = new Set<string>();
609
+ for (const raw of codes) {
610
+ const entry = canonicalizeGroupClaimCodeEntry(raw);
611
+ if (!entry) {
612
+ continue;
613
+ }
614
+ if (
615
+ entry.usedAt ||
616
+ isExpiredWithTtl({
617
+ createdAt: entry.createdAt,
618
+ nowMs,
619
+ ttlMs: GROUP_CLAIM_CODE_TTL_MS,
620
+ })
621
+ ) {
622
+ continue;
623
+ }
624
+ activeCodes.add(entry.code);
625
+ nextCodes.push(persistGroupClaimCodeEntry(entry));
626
+ }
627
+
628
+ let nextCode = "";
629
+ for (let attempt = 0; attempt < 10; attempt += 1) {
630
+ const candidate = randomCode(8);
631
+ if (!activeCodes.has(candidate)) {
632
+ nextCode = candidate;
633
+ break;
634
+ }
635
+ }
636
+ if (!nextCode) {
637
+ throw new Error("failed generating GeWe group claim code");
638
+ }
639
+
640
+ nextCodes.push({
641
+ code: nextCode,
642
+ ...(resolvedAccountId !== DEFAULT_ACCOUNT_ID ? { accountId: resolvedAccountId } : {}),
643
+ issuerId,
644
+ createdAt,
645
+ });
646
+ await writeJsonFileAtomically(filePath, {
647
+ version: 1,
648
+ codes: nextCodes,
649
+ } satisfies GeweGroupClaimCodeStore);
650
+ return nextCode;
651
+ });
652
+
653
+ return {
654
+ code,
655
+ accountId: resolvedAccountId,
656
+ issuerId,
657
+ createdAt,
658
+ expiresAt,
659
+ };
660
+ }
661
+
662
+ export async function redeemGeweGroupClaimCode(params: {
663
+ accountId?: string;
664
+ code: string;
665
+ issuerId: string;
666
+ groupId: string;
667
+ env?: NodeJS.ProcessEnv;
668
+ }): Promise<{
669
+ code: string;
670
+ accountId: string;
671
+ issuerId: string;
672
+ groupId: string;
673
+ createdAt?: string;
674
+ usedAt: string;
675
+ } | null> {
676
+ const resolvedAccountId = normalizeStoreAccountId(params.accountId);
677
+ const targetCode = normalizePairCode(params.code);
678
+ const issuerId = normalizeGroupClaimIssuerId(params.issuerId);
679
+ const groupId = stripChannelPrefix(params.groupId.trim());
680
+ if (!targetCode || !issuerId || !groupId) {
681
+ return null;
682
+ }
683
+ const filePath = resolveGeweGroupClaimCodesPath(resolvedAccountId, params.env);
684
+
685
+ const matched = await withFileLock(filePath, async () => {
686
+ const { value, exists } = await readJsonFileWithFallback<GeweGroupClaimCodeStore>(filePath, {
687
+ version: 1,
688
+ codes: [],
689
+ });
690
+ const nowMs = Date.now();
691
+ const usedAt = new Date().toISOString();
692
+ const codes = Array.isArray(value.codes) ? value.codes : [];
693
+ let result:
694
+ | {
695
+ code: string;
696
+ accountId: string;
697
+ issuerId: string;
698
+ groupId: string;
699
+ createdAt?: string;
700
+ usedAt: string;
701
+ }
702
+ | null = null;
703
+ let changed = false;
704
+ const nextCodes: GeweGroupClaimCodeEntry[] = [];
705
+ for (const raw of codes) {
706
+ const entry = canonicalizeGroupClaimCodeEntry(raw);
707
+ if (!entry) {
708
+ changed = true;
709
+ continue;
710
+ }
711
+ if (
712
+ entry.usedAt ||
713
+ isExpiredWithTtl({
714
+ createdAt: entry.createdAt,
715
+ nowMs,
716
+ ttlMs: GROUP_CLAIM_CODE_TTL_MS,
717
+ })
718
+ ) {
719
+ changed = true;
720
+ continue;
721
+ }
722
+ if (
723
+ !result &&
724
+ entry.code === targetCode &&
725
+ entry.accountId === resolvedAccountId &&
726
+ entry.issuerId === issuerId
727
+ ) {
728
+ changed = true;
729
+ result = {
730
+ code: entry.code,
731
+ accountId: entry.accountId,
732
+ issuerId: entry.issuerId,
733
+ groupId,
734
+ createdAt: entry.createdAt,
735
+ usedAt,
736
+ };
737
+ nextCodes.push(
738
+ persistGroupClaimCodeEntry({
739
+ ...entry,
740
+ usedAt,
741
+ usedGroupId: groupId,
742
+ }),
743
+ );
744
+ continue;
745
+ }
746
+ nextCodes.push(persistGroupClaimCodeEntry(entry));
747
+ }
748
+
749
+ if (changed || exists) {
750
+ await writeJsonFileAtomically(filePath, {
751
+ version: 1,
752
+ codes: nextCodes,
753
+ } satisfies GeweGroupClaimCodeStore);
754
+ }
755
+ return result;
756
+ });
757
+
758
+ return matched;
759
+ }
package/src/send.ts CHANGED
@@ -196,6 +196,7 @@ export async function sendAppMsgGewe(params: {
196
196
  account: ResolvedGeweAccount;
197
197
  toWxid: string;
198
198
  appmsg: string;
199
+ ats?: string;
199
200
  }): Promise<GeweSendResult> {
200
201
  const ctx = buildContext(params.account);
201
202
  const resp = await postGeweJson<GeweSendResponseData>({
@@ -206,6 +207,7 @@ export async function sendAppMsgGewe(params: {
206
207
  appId: ctx.appId,
207
208
  toWxid: params.toWxid,
208
209
  appmsg: params.appmsg,
210
+ ...(params.ats ? { ats: params.ats } : {}),
209
211
  },
210
212
  });
211
213
  const data = assertGeweOk(resp, "postAppMsg");
@@ -0,0 +1,27 @@
1
+ import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk";
2
+
3
+ import { CHANNEL_ALIASES, CHANNEL_ID } from "./constants.js";
4
+
5
+ const GEWE_DIRECT_SESSION_MARKERS = [
6
+ `:${CHANNEL_ID}:direct:`,
7
+ `:${CHANNEL_ID}:dm:`,
8
+ ":gewe:direct:",
9
+ ":gewe:dm:",
10
+ ];
11
+
12
+ export function isCurrentGeweDirectSession(ctx: OpenClawPluginToolContext): boolean {
13
+ const normalizedChannel = (ctx.messageChannel ?? "").trim().toLowerCase();
14
+ if (normalizedChannel && !CHANNEL_ALIASES.includes(normalizedChannel as (typeof CHANNEL_ALIASES)[number])) {
15
+ return false;
16
+ }
17
+
18
+ const normalizedSessionKey = (ctx.sessionKey ?? "").trim().toLowerCase();
19
+ if (!normalizedSessionKey) {
20
+ return false;
21
+ }
22
+ return GEWE_DIRECT_SESSION_MARKERS.some((marker) => normalizedSessionKey.includes(marker));
23
+ }
24
+
25
+ export function shouldExposeGeweAgentTool(ctx: OpenClawPluginToolContext): boolean {
26
+ return ctx.senderIsOwner === true || isCurrentGeweDirectSession(ctx);
27
+ }