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 +75 -1
- package/index.ts +2 -0
- package/package.json +1 -1
- package/skills/gewe-agent-tools/SKILL.md +40 -1
- package/src/api-tools.ts +15 -10
- package/src/delivery.ts +13 -7
- package/src/group-allowlist-tool.ts +11 -12
- package/src/group-binding-tool.ts +10 -4
- package/src/group-claim-tool.ts +103 -0
- package/src/inbound.ts +153 -14
- package/src/openclaw-compat.ts +17 -6
- package/src/pairing-store.ts +281 -0
- package/src/send.ts +2 -0
- package/src/tool-visibility.ts +27 -0
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
|
|
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
|
@@ -14,12 +14,13 @@ metadata:
|
|
|
14
14
|
|
|
15
15
|
# GeWe Agent Tools
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
? `<msgsource><atuserlist>${safeAtWxid}</atuserlist></msgsource>`
|
|
788
|
-
: "";
|
|
789
784
|
const partialTextXml = buildPartialQuoteXml(params.partialText);
|
|
790
|
-
return `<appmsg><title>${safeTitle}</title><type>57</type><refermsg>${partialTextXml}<svrid>${safeSvrid}</svrid
|
|
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 {
|
|
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, ...
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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;
|
package/src/openclaw-compat.ts
CHANGED
|
@@ -567,13 +567,10 @@ export function logInboundDrop(params: {
|
|
|
567
567
|
}
|
|
568
568
|
|
|
569
569
|
export function buildChannelConfigSchema(schema: ZodTypeAny): ChannelConfigSchema {
|
|
570
|
-
const
|
|
571
|
-
if (
|
|
570
|
+
const jsonSchema = buildJsonSchema(schema);
|
|
571
|
+
if (jsonSchema) {
|
|
572
572
|
return {
|
|
573
|
-
schema:
|
|
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
|
|
package/src/pairing-store.ts
CHANGED
|
@@ -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
|
+
}
|