a2a-xmtp 1.0.1 → 1.1.0
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/package.json +2 -2
- package/src/index.ts +89 -7
- package/src/tools/xmtp-group.ts +152 -0
- package/src/tools/xmtp-send.ts +27 -18
- package/src/types.ts +8 -0
- package/src/xmtp-bridge.ts +89 -11
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { XmtpBridge } from "./xmtp-bridge.js";
|
|
|
11
11
|
import { handleXmtpSend } from "./tools/xmtp-send.js";
|
|
12
12
|
import { handleXmtpInbox } from "./tools/xmtp-inbox.js";
|
|
13
13
|
import { handleDiscoverAgents } from "./tools/xmtp-agents.js";
|
|
14
|
-
import {
|
|
14
|
+
import { handleXmtpGroup } from "./tools/xmtp-group.js";
|
|
15
|
+
import { DEFAULT_PLUGIN_CONFIG, formatA2AMessage, type PluginConfig, type A2AInjectPayload } from "./types.js";
|
|
15
16
|
|
|
16
17
|
const AGENT_ID = "main"; // OpenClaw 默认 agent
|
|
17
18
|
|
|
@@ -33,15 +34,15 @@ export default definePluginEntry({
|
|
|
33
34
|
"Send an E2EE message to another agent via XMTP. " +
|
|
34
35
|
"Supports cross-gateway and cross-organization communication.",
|
|
35
36
|
parameters: Type.Object({
|
|
36
|
-
to: Type.String({ description: "Target agent ID or XMTP address (0x...)" }),
|
|
37
|
+
to: Type.Optional(Type.String({ description: "Target agent ID or XMTP address (0x...). Optional when conversationId is provided." })),
|
|
37
38
|
message: Type.String({ description: "Message content to send" }),
|
|
38
|
-
conversationId: Type.Optional(Type.String({ description: "Reuse existing conversation" })),
|
|
39
|
+
conversationId: Type.Optional(Type.String({ description: "Reuse existing conversation. When set, 'to' is optional." })),
|
|
39
40
|
contentType: Type.Optional(
|
|
40
41
|
Type.String({ description: "Message type: text or markdown", default: "text" }),
|
|
41
42
|
),
|
|
42
43
|
}),
|
|
43
44
|
async execute(_toolCallId, params) {
|
|
44
|
-
const p = params as { to
|
|
45
|
+
const p = params as { to?: string; message: string; conversationId?: string; contentType?: "text" | "markdown" };
|
|
45
46
|
return await handleXmtpSend(bridges, registry, policyEngine, p, AGENT_ID);
|
|
46
47
|
},
|
|
47
48
|
});
|
|
@@ -77,6 +78,23 @@ export default definePluginEntry({
|
|
|
77
78
|
},
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
api.registerTool({
|
|
82
|
+
name: "xmtp_group",
|
|
83
|
+
description:
|
|
84
|
+
"Manage XMTP group conversations. Actions: create (new group), list (all groups), " +
|
|
85
|
+
"members (view members), add_member, remove_member.",
|
|
86
|
+
parameters: Type.Object({
|
|
87
|
+
action: Type.String({ description: "Action: create | list | members | add_member | remove_member" }),
|
|
88
|
+
members: Type.Optional(Type.Array(Type.String(), { description: "Agent IDs or 0x addresses (for create/add_member/remove_member)" })),
|
|
89
|
+
conversationId: Type.Optional(Type.String({ description: "Group conversation ID (for members/add_member/remove_member)" })),
|
|
90
|
+
name: Type.Optional(Type.String({ description: "Group name (for create)" })),
|
|
91
|
+
}),
|
|
92
|
+
async execute(_toolCallId, params) {
|
|
93
|
+
const p = params as { action: string; members?: string[]; conversationId?: string; name?: string };
|
|
94
|
+
return await handleXmtpGroup(bridges, registry, p, AGENT_ID);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
80
98
|
// ── 2. 注册 HTTP 状态路由 ──
|
|
81
99
|
|
|
82
100
|
api.registerHttpRoute({
|
|
@@ -141,9 +159,73 @@ export default definePluginEntry({
|
|
|
141
159
|
config.xmtp.dbPath,
|
|
142
160
|
);
|
|
143
161
|
|
|
144
|
-
// 消息回调:收到 XMTP
|
|
145
|
-
bridge.onMessage = (agentId,
|
|
146
|
-
|
|
162
|
+
// 消息回调:收到 XMTP 消息时触发 subagent 进行 LLM 推理并自动回复
|
|
163
|
+
bridge.onMessage = async (agentId, payload: A2AInjectPayload) => {
|
|
164
|
+
const sessionKey = `xmtp:${payload.conversation.id}`;
|
|
165
|
+
const formattedMsg = formatA2AMessage(payload);
|
|
166
|
+
const senderLabel = payload.from.agentId || payload.from.xmtpAddress;
|
|
167
|
+
|
|
168
|
+
const extraSystemPrompt = [
|
|
169
|
+
`你收到了一条 XMTP 消息,来自 ${senderLabel}。`,
|
|
170
|
+
payload.conversation.isGroup
|
|
171
|
+
? `这是群聊,参与者: ${payload.conversation.participants.join(", ")}。`
|
|
172
|
+
: `这是私聊。`,
|
|
173
|
+
`请直接用文本回复,不要调用任何工具(不要调用 xmtp_send、xmtp_inbox 等)。`,
|
|
174
|
+
`系统会自动将你的文本回复发送给对方。`,
|
|
175
|
+
].join("\n");
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const { runId } = await api.runtime.subagent.run({
|
|
179
|
+
sessionKey,
|
|
180
|
+
message: formattedMsg,
|
|
181
|
+
extraSystemPrompt,
|
|
182
|
+
deliver: false,
|
|
183
|
+
idempotencyKey: `xmtp:${payload.message.id}`,
|
|
184
|
+
});
|
|
185
|
+
const result = await api.runtime.subagent.waitForRun({ runId, timeoutMs: 60000 });
|
|
186
|
+
if (result.status === "error") {
|
|
187
|
+
ctx.logger.error(`[a2a-xmtp] Subagent error: ${result.error}`);
|
|
188
|
+
} else if (result.status === "timeout") {
|
|
189
|
+
ctx.logger.warn(`[a2a-xmtp] Subagent timeout for ${sessionKey}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 取回 LLM 的文本回复,手动通过 XMTP 发送
|
|
193
|
+
if (result.status === "ok") {
|
|
194
|
+
const { messages } = await api.runtime.subagent.getSessionMessages({
|
|
195
|
+
sessionKey,
|
|
196
|
+
limit: 5,
|
|
197
|
+
});
|
|
198
|
+
// 找到最后一条 assistant 回复
|
|
199
|
+
const lastReply = [...messages].reverse().find(
|
|
200
|
+
(m: any) => m.role === "assistant" && m.content,
|
|
201
|
+
);
|
|
202
|
+
if (lastReply) {
|
|
203
|
+
const rawContent = (lastReply as any).content;
|
|
204
|
+
// content 可能是 string 或 content blocks 数组(含 thinking/text)
|
|
205
|
+
let replyText: string;
|
|
206
|
+
if (typeof rawContent === "string") {
|
|
207
|
+
replyText = rawContent;
|
|
208
|
+
} else if (Array.isArray(rawContent)) {
|
|
209
|
+
// 只提取 type=text 的部分,跳过 thinking
|
|
210
|
+
replyText = rawContent
|
|
211
|
+
.filter((block: any) => block.type === "text" && block.text)
|
|
212
|
+
.map((block: any) => block.text)
|
|
213
|
+
.join("\n");
|
|
214
|
+
} else {
|
|
215
|
+
replyText = String(rawContent);
|
|
216
|
+
}
|
|
217
|
+
if (!replyText.trim()) return; // 没有有效文本则不回复
|
|
218
|
+
await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
|
|
219
|
+
conversationId: payload.conversation.id,
|
|
220
|
+
});
|
|
221
|
+
ctx.logger.info(`[a2a-xmtp] Replied to ${senderLabel} in ${payload.conversation.id}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
ctx.logger.error(
|
|
226
|
+
`[a2a-xmtp] Failed to trigger subagent: ${err instanceof Error ? err.message : String(err)}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
147
229
|
};
|
|
148
230
|
|
|
149
231
|
await bridge.start();
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Tool: xmtp_group — 群聊管理(创建、列表、成员查看/添加/移除)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import type { XmtpBridge } from "../xmtp-bridge.js";
|
|
6
|
+
import type { IdentityRegistry } from "../identity-registry.js";
|
|
7
|
+
|
|
8
|
+
type GroupAction = "create" | "list" | "members" | "add_member" | "remove_member";
|
|
9
|
+
|
|
10
|
+
export async function handleXmtpGroup(
|
|
11
|
+
bridges: Map<string, XmtpBridge>,
|
|
12
|
+
registry: IdentityRegistry,
|
|
13
|
+
params: {
|
|
14
|
+
action: string;
|
|
15
|
+
members?: string[];
|
|
16
|
+
conversationId?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
},
|
|
19
|
+
agentId: string,
|
|
20
|
+
) {
|
|
21
|
+
const bridge = bridges.get(agentId) ?? bridges.values().next().value;
|
|
22
|
+
if (!bridge) {
|
|
23
|
+
return { content: [{ type: "text" as const, text: "No XMTP bridge available. Plugin not initialized." }] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const action = params.action as GroupAction;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
switch (action) {
|
|
30
|
+
case "create": {
|
|
31
|
+
if (!params.members?.length) {
|
|
32
|
+
return { content: [{ type: "text" as const, text: "Parameter 'members' is required for create action. Provide agent IDs or 0x addresses." }] };
|
|
33
|
+
}
|
|
34
|
+
// 解析成员地址
|
|
35
|
+
const addresses: string[] = [];
|
|
36
|
+
for (const member of params.members) {
|
|
37
|
+
if (member.startsWith("0x")) {
|
|
38
|
+
addresses.push(member);
|
|
39
|
+
} else {
|
|
40
|
+
const addr = registry.getAddress(member);
|
|
41
|
+
if (!addr) {
|
|
42
|
+
return { content: [{ type: "text" as const, text: `Agent "${member}" not found. Use xmtp_agents to discover available agents.` }] };
|
|
43
|
+
}
|
|
44
|
+
addresses.push(addr);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await bridge.createGroup(addresses, params.name);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text" as const, text: `Group created${result.name ? ` "${result.name}"` : ""} with ${addresses.length} members.` }],
|
|
51
|
+
details: {
|
|
52
|
+
action: "create",
|
|
53
|
+
conversationId: result.conversationId,
|
|
54
|
+
name: result.name,
|
|
55
|
+
memberCount: addresses.length,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "list": {
|
|
61
|
+
const groups = await bridge.listGroups();
|
|
62
|
+
if (groups.length === 0) {
|
|
63
|
+
return { content: [{ type: "text" as const, text: "No group conversations found." }] };
|
|
64
|
+
}
|
|
65
|
+
const lines = groups.map((g) => {
|
|
66
|
+
const nameLabel = g.name ? ` "${g.name}"` : "";
|
|
67
|
+
return `- ${g.conversationId}${nameLabel} (${g.memberAddresses.length} members, created ${g.createdAt})`;
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text" as const, text: `Groups:\n${lines.join("\n")}` }],
|
|
71
|
+
details: { action: "list", count: groups.length, groups },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case "members": {
|
|
76
|
+
if (!params.conversationId) {
|
|
77
|
+
return { content: [{ type: "text" as const, text: "Parameter 'conversationId' is required for members action." }] };
|
|
78
|
+
}
|
|
79
|
+
const members = await bridge.getGroupMembers(params.conversationId);
|
|
80
|
+
// 尝试解析 agentId
|
|
81
|
+
const resolved = members.map((addr) => {
|
|
82
|
+
const aid = registry.getAgentId(addr);
|
|
83
|
+
return aid ? `${aid} (${addr})` : addr;
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text" as const, text: `Members (${members.length}):\n${resolved.map((m) => `- ${m}`).join("\n")}` }],
|
|
87
|
+
details: { action: "members", conversationId: params.conversationId, count: members.length, members },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "add_member": {
|
|
92
|
+
if (!params.conversationId) {
|
|
93
|
+
return { content: [{ type: "text" as const, text: "Parameter 'conversationId' is required for add_member action." }] };
|
|
94
|
+
}
|
|
95
|
+
if (!params.members?.length) {
|
|
96
|
+
return { content: [{ type: "text" as const, text: "Parameter 'members' is required for add_member action." }] };
|
|
97
|
+
}
|
|
98
|
+
const addresses: string[] = [];
|
|
99
|
+
for (const member of params.members) {
|
|
100
|
+
if (member.startsWith("0x")) {
|
|
101
|
+
addresses.push(member);
|
|
102
|
+
} else {
|
|
103
|
+
const addr = registry.getAddress(member);
|
|
104
|
+
if (!addr) {
|
|
105
|
+
return { content: [{ type: "text" as const, text: `Agent "${member}" not found.` }] };
|
|
106
|
+
}
|
|
107
|
+
addresses.push(addr);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await bridge.addGroupMembers(params.conversationId, addresses);
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text" as const, text: `Added ${addresses.length} member(s) to group.` }],
|
|
113
|
+
details: { action: "add_member", conversationId: params.conversationId, added: addresses },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case "remove_member": {
|
|
118
|
+
if (!params.conversationId) {
|
|
119
|
+
return { content: [{ type: "text" as const, text: "Parameter 'conversationId' is required for remove_member action." }] };
|
|
120
|
+
}
|
|
121
|
+
if (!params.members?.length) {
|
|
122
|
+
return { content: [{ type: "text" as const, text: "Parameter 'members' is required for remove_member action." }] };
|
|
123
|
+
}
|
|
124
|
+
const addresses: string[] = [];
|
|
125
|
+
for (const member of params.members) {
|
|
126
|
+
if (member.startsWith("0x")) {
|
|
127
|
+
addresses.push(member);
|
|
128
|
+
} else {
|
|
129
|
+
const addr = registry.getAddress(member);
|
|
130
|
+
if (!addr) {
|
|
131
|
+
return { content: [{ type: "text" as const, text: `Agent "${member}" not found.` }] };
|
|
132
|
+
}
|
|
133
|
+
addresses.push(addr);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await bridge.removeGroupMembers(params.conversationId, addresses);
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text" as const, text: `Removed ${addresses.length} member(s) from group.` }],
|
|
139
|
+
details: { action: "remove_member", conversationId: params.conversationId, removed: addresses },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
default:
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: "text" as const, text: `Unknown action "${action}". Supported: create, list, members, add_member, remove_member.` }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
} catch (err: unknown) {
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
return { content: [{ type: "text" as const, text: `Group operation failed: ${msg}` }] };
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/tools/xmtp-send.ts
CHANGED
|
@@ -10,7 +10,7 @@ export async function handleXmtpSend(
|
|
|
10
10
|
bridges: Map<string, XmtpBridge>,
|
|
11
11
|
registry: IdentityRegistry,
|
|
12
12
|
policyEngine: PolicyEngine,
|
|
13
|
-
params: { to
|
|
13
|
+
params: { to?: string; message: string; conversationId?: string; contentType?: "text" | "markdown" },
|
|
14
14
|
senderAgentId: string,
|
|
15
15
|
) {
|
|
16
16
|
// 找到发送者 bridge(使用第一个可用的 bridge)
|
|
@@ -19,39 +19,48 @@ export async function handleXmtpSend(
|
|
|
19
19
|
return { content: [{ type: "text" as const, text: "No XMTP bridge available. Plugin not initialized." }] };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
targetAddress = params.to;
|
|
26
|
-
} else {
|
|
27
|
-
const addr = registry.getAddress(params.to);
|
|
28
|
-
if (!addr) {
|
|
29
|
-
return {
|
|
30
|
-
content: [{ type: "text" as const, text: `Agent "${params.to}" not found. Use xmtp_agents to discover available agents.` }],
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
targetAddress = addr;
|
|
22
|
+
// 当提供 conversationId 时,to 可省略(直接向已有会话发送)
|
|
23
|
+
if (!params.to && !params.conversationId) {
|
|
24
|
+
return { content: [{ type: "text" as const, text: "Either 'to' or 'conversationId' must be provided." }] };
|
|
34
25
|
}
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
// 解析目标地址(仅在提供 to 时需要)
|
|
28
|
+
let targetAddress: string | undefined;
|
|
29
|
+
if (params.to) {
|
|
30
|
+
if (params.to.startsWith("0x")) {
|
|
31
|
+
targetAddress = params.to;
|
|
32
|
+
} else {
|
|
33
|
+
const addr = registry.getAddress(params.to);
|
|
34
|
+
if (!addr) {
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text" as const, text: `Agent "${params.to}" not found. Use xmtp_agents to discover available agents.` }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
targetAddress = addr;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (targetAddress.toLowerCase() === bridge.address.toLowerCase()) {
|
|
43
|
+
return { content: [{ type: "text" as const, text: "Cannot send message to yourself." }] };
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
// Policy 检查
|
|
48
|
+
const toLabel = params.to ?? params.conversationId!;
|
|
41
49
|
const convId = params.conversationId ?? `dm:${senderAgentId}:${params.to}`;
|
|
42
|
-
const check = policyEngine.checkOutgoing({ from: senderAgentId, to:
|
|
50
|
+
const check = policyEngine.checkOutgoing({ from: senderAgentId, to: toLabel, conversationId: convId });
|
|
43
51
|
if (!check.allowed) {
|
|
44
52
|
return { content: [{ type: "text" as const, text: `Blocked: ${check.reason}` }] };
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
try {
|
|
48
|
-
const result = await bridge.sendMessage(targetAddress, params.message, {
|
|
56
|
+
const result = await bridge.sendMessage(targetAddress ?? "", params.message, {
|
|
49
57
|
contentType: params.contentType,
|
|
50
58
|
conversationId: params.conversationId,
|
|
51
59
|
});
|
|
52
60
|
|
|
61
|
+
const recipientLabel = params.to ?? `conversation ${result.conversationId}`;
|
|
53
62
|
return {
|
|
54
|
-
content: [{ type: "text" as const, text: `Message sent to ${
|
|
63
|
+
content: [{ type: "text" as const, text: `Message sent to ${recipientLabel}.` }],
|
|
55
64
|
details: {
|
|
56
65
|
status: "sent",
|
|
57
66
|
conversationId: result.conversationId,
|
package/src/types.ts
CHANGED
|
@@ -81,6 +81,14 @@ export interface InboxMessage {
|
|
|
81
81
|
timestamp: string;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** 群聊信息 */
|
|
85
|
+
export interface GroupInfo {
|
|
86
|
+
conversationId: string;
|
|
87
|
+
name: string;
|
|
88
|
+
memberAddresses: string[];
|
|
89
|
+
createdAt: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
/** 默认策略配置 */
|
|
85
93
|
export const DEFAULT_POLICY: ConversationPolicy = {
|
|
86
94
|
maxTurns: 10,
|
package/src/xmtp-bridge.ts
CHANGED
|
@@ -6,19 +6,20 @@
|
|
|
6
6
|
import { Agent, createUser, createSigner, IdentifierKind } from "@xmtp/agent-sdk";
|
|
7
7
|
import type { IdentityRegistry } from "./identity-registry.js";
|
|
8
8
|
import { PolicyEngine } from "./policy-engine.js";
|
|
9
|
-
import type { XmtpWalletConfig, InboxMessage, A2AContentType } from "./types.js";
|
|
10
|
-
import { createA2APayload
|
|
9
|
+
import type { XmtpWalletConfig, InboxMessage, A2AContentType, A2AInjectPayload, GroupInfo } from "./types.js";
|
|
10
|
+
import { createA2APayload } from "./types.js";
|
|
11
11
|
|
|
12
12
|
export class XmtpBridge {
|
|
13
13
|
private agent: InstanceType<typeof Agent> | null = null;
|
|
14
14
|
private running = false;
|
|
15
|
+
private syncTimer: ReturnType<typeof setInterval> | null = null;
|
|
15
16
|
|
|
16
17
|
/** 收件箱缓存 */
|
|
17
18
|
private inboxBuffer: InboxMessage[] = [];
|
|
18
19
|
private readonly maxInboxBuffer = 100;
|
|
19
20
|
|
|
20
|
-
/** 消息回调(由 plugin
|
|
21
|
-
onMessage?: (agentId: string,
|
|
21
|
+
/** 消息回调(由 plugin 入口设置,用于触发 subagent 处理) */
|
|
22
|
+
onMessage?: (agentId: string, payload: A2AInjectPayload) => void;
|
|
22
23
|
|
|
23
24
|
constructor(
|
|
24
25
|
readonly agentId: string,
|
|
@@ -52,10 +53,21 @@ export class XmtpBridge {
|
|
|
52
53
|
|
|
53
54
|
await this.agent.start();
|
|
54
55
|
this.running = true;
|
|
56
|
+
|
|
57
|
+
// 定期同步会话列表,发现被外部添加到的新群聊(每 30 秒)
|
|
58
|
+
this.syncTimer = setInterval(async () => {
|
|
59
|
+
try {
|
|
60
|
+
await this.agent?.client.conversations.sync();
|
|
61
|
+
} catch { /* 同步失败不影响正常运行 */ }
|
|
62
|
+
}, 30_000);
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
async stop(): Promise<void> {
|
|
58
66
|
if (!this.running || !this.agent) return;
|
|
67
|
+
if (this.syncTimer) {
|
|
68
|
+
clearInterval(this.syncTimer);
|
|
69
|
+
this.syncTimer = null;
|
|
70
|
+
}
|
|
59
71
|
await this.agent.stop();
|
|
60
72
|
this.running = false;
|
|
61
73
|
}
|
|
@@ -96,10 +108,64 @@ export class XmtpBridge {
|
|
|
96
108
|
return messages.slice(0, limit);
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
async createGroup(memberAddresses: string[]): Promise<string> {
|
|
111
|
+
async createGroup(memberAddresses: string[], name?: string): Promise<{ conversationId: string; name: string }> {
|
|
112
|
+
if (!this.agent) throw new Error(`Bridge for ${this.agentId} not started`);
|
|
113
|
+
const opts = name ? { groupName: name } : undefined;
|
|
114
|
+
const group = await (this.agent as any).createGroupWithAddresses(memberAddresses, opts);
|
|
115
|
+
if (name) {
|
|
116
|
+
try { await group.updateName(name); } catch { /* 部分 SDK 版本 createGroupWithAddresses 不支持 opts */ }
|
|
117
|
+
}
|
|
118
|
+
return { conversationId: group.id, name: group.name ?? name ?? "" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async listGroups(): Promise<GroupInfo[]> {
|
|
122
|
+
if (!this.agent) throw new Error(`Bridge for ${this.agentId} not started`);
|
|
123
|
+
const conversations = await this.agent.client.conversations.list();
|
|
124
|
+
const groups: GroupInfo[] = [];
|
|
125
|
+
for (const conv of conversations) {
|
|
126
|
+
const meta = await conv.metadata();
|
|
127
|
+
if (meta.conversationType !== 1) continue; // 1 = Group, 0 = Dm
|
|
128
|
+
const members = await conv.members();
|
|
129
|
+
groups.push({
|
|
130
|
+
conversationId: conv.id,
|
|
131
|
+
name: (conv as any).name ?? "",
|
|
132
|
+
memberAddresses: members.map((m: any) =>
|
|
133
|
+
m.accountAddresses?.[0]?.toLowerCase() ?? m.inboxId,
|
|
134
|
+
),
|
|
135
|
+
createdAt: conv.createdAt.toISOString(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return groups;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getGroupMembers(conversationId: string): Promise<string[]> {
|
|
100
142
|
if (!this.agent) throw new Error(`Bridge for ${this.agentId} not started`);
|
|
101
|
-
const
|
|
102
|
-
|
|
143
|
+
const conv = await this.agent.client.conversations.getConversationById(conversationId);
|
|
144
|
+
if (!conv) throw new Error(`Conversation ${conversationId} not found`);
|
|
145
|
+
const members = await conv.members();
|
|
146
|
+
return members.map((m: any) => m.accountAddresses?.[0]?.toLowerCase() ?? m.inboxId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async addGroupMembers(conversationId: string, addresses: string[]): Promise<void> {
|
|
150
|
+
if (!this.agent) throw new Error(`Bridge for ${this.agentId} not started`);
|
|
151
|
+
const conv = await this.agent.client.conversations.getConversationById(conversationId);
|
|
152
|
+
if (!conv) throw new Error(`Conversation ${conversationId} not found`);
|
|
153
|
+
const identifiers = addresses.map((addr) => ({
|
|
154
|
+
identifier: addr,
|
|
155
|
+
identifierKind: IdentifierKind.Ethereum,
|
|
156
|
+
}));
|
|
157
|
+
await (conv as any).addMembersByIdentifiers(identifiers);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async removeGroupMembers(conversationId: string, addresses: string[]): Promise<void> {
|
|
161
|
+
if (!this.agent) throw new Error(`Bridge for ${this.agentId} not started`);
|
|
162
|
+
const conv = await this.agent.client.conversations.getConversationById(conversationId);
|
|
163
|
+
if (!conv) throw new Error(`Conversation ${conversationId} not found`);
|
|
164
|
+
const identifiers = addresses.map((addr) => ({
|
|
165
|
+
identifier: addr,
|
|
166
|
+
identifierKind: IdentifierKind.Ethereum,
|
|
167
|
+
}));
|
|
168
|
+
await (conv as any).removeMembersByIdentifiers(identifiers);
|
|
103
169
|
}
|
|
104
170
|
|
|
105
171
|
async canMessage(address: string): Promise<boolean> {
|
|
@@ -130,12 +196,24 @@ export class XmtpBridge {
|
|
|
130
196
|
|
|
131
197
|
this.policyEngine.recordTurn(ctx.conversation.id, depth + 1);
|
|
132
198
|
|
|
199
|
+
// 群聊时获取参与者列表
|
|
200
|
+
let participants: string[] = [];
|
|
201
|
+
const isGroup = ctx.conversation.isGroup ?? false;
|
|
202
|
+
if (isGroup) {
|
|
203
|
+
try {
|
|
204
|
+
const members = await ctx.conversation.members();
|
|
205
|
+
participants = members.map((m: any) =>
|
|
206
|
+
m.accountAddresses?.[0]?.toLowerCase() ?? m.inboxId,
|
|
207
|
+
);
|
|
208
|
+
} catch { /* 获取成员失败时保持空数组 */ }
|
|
209
|
+
}
|
|
210
|
+
|
|
133
211
|
const payload = createA2APayload({
|
|
134
212
|
fromAgentId: senderAgentId,
|
|
135
213
|
fromAddress: senderAddress,
|
|
136
214
|
conversationId: ctx.conversation.id,
|
|
137
|
-
isGroup
|
|
138
|
-
participants
|
|
215
|
+
isGroup,
|
|
216
|
+
participants,
|
|
139
217
|
messageId: ctx.message.id ?? crypto.randomUUID(),
|
|
140
218
|
content: String(ctx.message.content),
|
|
141
219
|
contentType,
|
|
@@ -154,9 +232,9 @@ export class XmtpBridge {
|
|
|
154
232
|
});
|
|
155
233
|
if (this.inboxBuffer.length > this.maxInboxBuffer) this.inboxBuffer.pop();
|
|
156
234
|
|
|
157
|
-
// 通知 plugin
|
|
235
|
+
// 通知 plugin 层(传递结构化 payload,由调用方决定如何处理)
|
|
158
236
|
if (this.onMessage) {
|
|
159
|
-
this.onMessage(this.agentId,
|
|
237
|
+
this.onMessage(this.agentId, payload);
|
|
160
238
|
}
|
|
161
239
|
}
|
|
162
240
|
|