palz-connector 1.2.1 → 1.2.2
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/palz-connector.staging.config.json +2 -2
- package/src/bot.ts +189 -24
- package/src/channel.ts +1 -1
- package/src/dedup.ts +11 -6
- package/src/outbound.ts +6 -4
- package/src/targets.ts +10 -2
- package/src/types.ts +3 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"enabled": true,
|
|
3
|
-
"streamUrl": "wss://claw-server.
|
|
4
|
-
"apiBaseUrl": "https://claw-server.
|
|
3
|
+
"streamUrl": "wss://claw-server.csagentai.com/ws/bot",
|
|
4
|
+
"apiBaseUrl": "https://claw-server.csagentai.com/api",
|
|
5
5
|
"sessionTimeout": 1800000
|
|
6
6
|
}
|
package/src/bot.ts
CHANGED
|
@@ -46,6 +46,91 @@ function createChatQueue() {
|
|
|
46
46
|
|
|
47
47
|
const enqueue = createChatQueue();
|
|
48
48
|
|
|
49
|
+
// ============ 群聊历史记录 ============
|
|
50
|
+
|
|
51
|
+
interface HistoryEntry {
|
|
52
|
+
sender: string;
|
|
53
|
+
body: string;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
messageId: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_GROUP_HISTORY_LIMIT = 50;
|
|
59
|
+
const MAX_HISTORY_KEYS = 3000;
|
|
60
|
+
|
|
61
|
+
/** 群聊历史缓存,key = historyKey(agentId:conversationId) */
|
|
62
|
+
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
63
|
+
|
|
64
|
+
function recordGroupHistoryEntry(params: {
|
|
65
|
+
historyKey: string;
|
|
66
|
+
entry: HistoryEntry;
|
|
67
|
+
limit: number;
|
|
68
|
+
log?: (...args: any[]) => void;
|
|
69
|
+
}): void {
|
|
70
|
+
const log = params.log ?? console.log;
|
|
71
|
+
if (params.limit <= 0) {
|
|
72
|
+
log(`[HISTORY record] 跳过: limit=${params.limit} historyKey=${params.historyKey}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const history = chatHistories.get(params.historyKey) ?? [];
|
|
76
|
+
const beforeLen = history.length;
|
|
77
|
+
history.push(params.entry);
|
|
78
|
+
while (history.length > params.limit) {
|
|
79
|
+
history.shift();
|
|
80
|
+
}
|
|
81
|
+
// 刷新插入顺序(LRU)
|
|
82
|
+
if (chatHistories.has(params.historyKey)) {
|
|
83
|
+
chatHistories.delete(params.historyKey);
|
|
84
|
+
}
|
|
85
|
+
chatHistories.set(params.historyKey, history);
|
|
86
|
+
log(`[HISTORY record] historyKey=${params.historyKey} before=${beforeLen} after=${history.length} limit=${params.limit} sender=${params.entry.sender} msgId=${params.entry.messageId} body="${params.entry.body.slice(0, 80)}" totalKeys=${chatHistories.size}`);
|
|
87
|
+
// 超过最大 key 数量时淘汰最老的
|
|
88
|
+
if (chatHistories.size > MAX_HISTORY_KEYS) {
|
|
89
|
+
const keysToDelete = chatHistories.size - MAX_HISTORY_KEYS;
|
|
90
|
+
const iterator = chatHistories.keys();
|
|
91
|
+
for (let i = 0; i < keysToDelete; i++) {
|
|
92
|
+
const key = iterator.next().value;
|
|
93
|
+
if (key !== undefined) chatHistories.delete(key);
|
|
94
|
+
}
|
|
95
|
+
log(`[HISTORY evict] 淘汰了 ${keysToDelete} 个 key, 剩余 ${chatHistories.size}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildGroupHistoryContext(params: {
|
|
100
|
+
historyKey: string;
|
|
101
|
+
currentMessage: string;
|
|
102
|
+
formatEntry: (entry: HistoryEntry) => string;
|
|
103
|
+
log?: (...args: any[]) => void;
|
|
104
|
+
}): string {
|
|
105
|
+
const log = params.log ?? console.log;
|
|
106
|
+
const entries = chatHistories.get(params.historyKey) ?? [];
|
|
107
|
+
log(`[HISTORY build] historyKey=${params.historyKey} entriesCount=${entries.length} currentMsgLen=${params.currentMessage.length}`);
|
|
108
|
+
if (entries.length === 0) {
|
|
109
|
+
log(`[HISTORY build] 无历史记录, 返回原始消息`);
|
|
110
|
+
return params.currentMessage;
|
|
111
|
+
}
|
|
112
|
+
for (let i = 0; i < entries.length; i++) {
|
|
113
|
+
log(`[HISTORY build] entry[${i}] sender=${entries[i].sender} msgId=${entries[i].messageId} body="${entries[i].body.slice(0, 80)}" ts=${entries[i].timestamp}`);
|
|
114
|
+
}
|
|
115
|
+
const historyText = entries.map(params.formatEntry).join("\n");
|
|
116
|
+
const combined = [
|
|
117
|
+
"[Chat messages since your last reply - for context]",
|
|
118
|
+
historyText,
|
|
119
|
+
"",
|
|
120
|
+
"[Current message - respond to this]",
|
|
121
|
+
params.currentMessage,
|
|
122
|
+
].join("\n");
|
|
123
|
+
log(`[HISTORY build] 拼接完成: historyTextLen=${historyText.length} combinedLen=${combined.length}`);
|
|
124
|
+
return combined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clearGroupHistory(historyKey: string, log?: (...args: any[]) => void): void {
|
|
128
|
+
const _log = log ?? console.log;
|
|
129
|
+
const entries = chatHistories.get(historyKey) ?? [];
|
|
130
|
+
_log(`[HISTORY clear] historyKey=${historyKey} clearedEntries=${entries.length}`);
|
|
131
|
+
chatHistories.set(historyKey, []);
|
|
132
|
+
}
|
|
133
|
+
|
|
49
134
|
// ============ 媒体 payload 构建 ============
|
|
50
135
|
|
|
51
136
|
function buildMediaPayload(
|
|
@@ -80,7 +165,10 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
|
|
|
80
165
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
81
166
|
const tag = `palz[${accountId}]`;
|
|
82
167
|
|
|
83
|
-
|
|
168
|
+
const isGroup = msg.conversation_type === "group";
|
|
169
|
+
const effectiveAgentId = msg.agent_id || "main";
|
|
170
|
+
|
|
171
|
+
log(`${tag}: [STEP 1/6 入站过滤] msg_id=${msg.msg_id} sender=${msg.sender_id} conv=${msg.conversation_id} type=${msg.conversation_type} agent=${effectiveAgentId}`);
|
|
84
172
|
|
|
85
173
|
const content = msg.content;
|
|
86
174
|
if (!content) {
|
|
@@ -102,14 +190,38 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
|
|
|
102
190
|
return;
|
|
103
191
|
}
|
|
104
192
|
|
|
105
|
-
//
|
|
106
|
-
const
|
|
107
|
-
|
|
193
|
+
// 群聊 @提及检测
|
|
194
|
+
const wasMentioned = isGroup ? (msg.mentioned_bot === true) : true;
|
|
195
|
+
if (isGroup && !wasMentioned) {
|
|
196
|
+
// 未@机器人:记录到群聊历史,下次被@时作为上下文
|
|
197
|
+
const historyKey = `${effectiveAgentId}:${msg.conversation_id}`;
|
|
198
|
+
const senderName = msg.sender_name || msg.sender_id;
|
|
199
|
+
log(`${tag}: [STEP 1 群聊历史] 未@机器人, 准备记录历史 historyKey=${historyKey} mentioned_bot=${msg.mentioned_bot} conversation_type=${msg.conversation_type}`);
|
|
200
|
+
recordGroupHistoryEntry({
|
|
201
|
+
historyKey,
|
|
202
|
+
entry: {
|
|
203
|
+
sender: msg.sender_id,
|
|
204
|
+
body: `${senderName}: ${plainText}`,
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
messageId: msg.msg_id,
|
|
207
|
+
},
|
|
208
|
+
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
209
|
+
log,
|
|
210
|
+
});
|
|
211
|
+
log(`${tag}: [STEP 1 跳过] 原因=群聊中未@机器人, 已记录到历史 historyKey=${historyKey}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 去重(按 agentId + conversationId 隔离,同群多 bot 场景)
|
|
216
|
+
const claimed = tryClaimMessage(msg.msg_id, effectiveAgentId, msg.conversation_id);
|
|
217
|
+
log(`${tag}: [STEP 2/6 去重] msg_id=${msg.msg_id} agent=${effectiveAgentId} conv=${msg.conversation_id} claimed=${claimed}`);
|
|
108
218
|
if (!claimed) return;
|
|
109
219
|
|
|
110
|
-
//
|
|
111
|
-
const queueKey =
|
|
112
|
-
|
|
220
|
+
// 入队(按 agentId 隔离,不同 agent 并行处理)
|
|
221
|
+
const queueKey = isGroup
|
|
222
|
+
? `${effectiveAgentId}:${msg.conversation_id}`
|
|
223
|
+
: `${effectiveAgentId}:${msg.sender_id}:${msg.conversation_id}`;
|
|
224
|
+
log(`${tag}: [STEP 3/6 入队] queueKey="${queueKey}" isGroup=${isGroup}`);
|
|
113
225
|
|
|
114
226
|
enqueue(queueKey, async () => {
|
|
115
227
|
const startMs = Date.now();
|
|
@@ -130,9 +242,14 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
130
242
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
131
243
|
const config = account.config;
|
|
132
244
|
|
|
245
|
+
const isGroup = msg.conversation_type === "group";
|
|
133
246
|
const plainText = extractPlainText(msg.content).trim();
|
|
134
247
|
const useStream = msg.stream === true;
|
|
135
|
-
const
|
|
248
|
+
const senderName = msg.sender_name || msg.sender_id;
|
|
249
|
+
|
|
250
|
+
// 群聊:peerId = chat:conversation_id(整群共享 session,与 palzTo 格式一致)
|
|
251
|
+
// DM:peerId = sender_id:conversation_id(每用户每会话独立 session)
|
|
252
|
+
const peerId = isGroup ? `chat:${msg.conversation_id}` : `${msg.sender_id}:${msg.conversation_id}`;
|
|
136
253
|
|
|
137
254
|
// STEP 4: 解析媒体
|
|
138
255
|
log(`${tag}: [STEP 4/6 媒体解析] 输入: contentType=${typeof msg.content === "string" ? "string" : "array"} imageCount=${Array.isArray(msg.content) ? msg.content.filter((p: ContentPart) => p.type === "image_url").length : 0}`);
|
|
@@ -141,13 +258,14 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
141
258
|
log(`${tag}: [STEP 4 输出] mediaList=${JSON.stringify(mediaList.map((m) => ({ path: m.path, contentType: m.contentType })))} mediaPayload=${JSON.stringify(mediaPayload)}`);
|
|
142
259
|
|
|
143
260
|
// STEP 5: 解析路由
|
|
144
|
-
const
|
|
261
|
+
const peerKind = isGroup ? "group" : "direct";
|
|
262
|
+
const routeInput = { cfg: "(cfg)", channel: "palz-connector", accountId, peer: { kind: peerKind, id: peerId } };
|
|
145
263
|
log(`${tag}: [STEP 5/6 路由解析] 输入: ${JSON.stringify(routeInput)} agent_id=${msg.agent_id ?? "(auto)"}`);
|
|
146
264
|
const route = core.channel.routing.resolveAgentRoute({
|
|
147
265
|
cfg,
|
|
148
266
|
channel: "palz-connector",
|
|
149
267
|
accountId,
|
|
150
|
-
peer: { kind:
|
|
268
|
+
peer: { kind: peerKind, id: peerId },
|
|
151
269
|
});
|
|
152
270
|
|
|
153
271
|
// IM 指定 agent_id 时走指定 agent,否则强制走 main
|
|
@@ -163,11 +281,13 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
163
281
|
|
|
164
282
|
// STEP 6a: 构建 envelope body
|
|
165
283
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
166
|
-
const messageBody = `${
|
|
167
|
-
|
|
284
|
+
const messageBody = `${senderName}: ${plainText}`;
|
|
285
|
+
// 群聊 from 带上 conversation_id 以区分不同用户
|
|
286
|
+
const envelopeFrom = isGroup ? `${msg.conversation_id}:${msg.sender_id}` : msg.sender_id;
|
|
287
|
+
log(`${tag}: [STEP 6a/6 envelope构建] 输入: channel=Palz from=${envelopeFrom} messageBody="${messageBody.slice(0, 120)}" envelopeOptions=${JSON.stringify(envelopeOptions)}`);
|
|
168
288
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
169
289
|
channel: "Palz",
|
|
170
|
-
from:
|
|
290
|
+
from: envelopeFrom,
|
|
171
291
|
timestamp: new Date(msg.timestamp || Date.now()),
|
|
172
292
|
body: messageBody,
|
|
173
293
|
envelope: envelopeOptions,
|
|
@@ -176,10 +296,11 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
176
296
|
|
|
177
297
|
// STEP 6b: 构建 inbound context
|
|
178
298
|
const palzFrom = `palz:${msg.sender_id}`;
|
|
179
|
-
|
|
299
|
+
// 群聊:To 指向群,DM:To 指向用户会话
|
|
300
|
+
const palzTo = isGroup ? `chat:${msg.conversation_id}` : `${msg.sender_id}:${msg.conversation_id}`;
|
|
180
301
|
|
|
181
|
-
//
|
|
182
|
-
|
|
302
|
+
// 命令授权:DM 默认允许,群聊也默认允许(可后续扩展 allowlist)
|
|
303
|
+
const wasMentioned = isGroup ? (msg.mentioned_bot === true) : true;
|
|
183
304
|
const needsCommandAuth = core.channel.commands.shouldComputeCommandAuthorized(plainText, cfg);
|
|
184
305
|
const commandAuthorized = needsCommandAuth
|
|
185
306
|
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
@@ -189,23 +310,60 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
189
310
|
: undefined;
|
|
190
311
|
log(`${tag}: [STEP 6b 命令授权] needsCommandAuth=${needsCommandAuth} commandAuthorized=${commandAuthorized}`);
|
|
191
312
|
|
|
313
|
+
const chatType = isGroup ? "group" : "direct";
|
|
314
|
+
|
|
315
|
+
// 群聊历史:将积攒的未@消息拼入 Body 上下文
|
|
316
|
+
const historyKey = isGroup ? `${effectiveAgentId}:${msg.conversation_id}` : undefined;
|
|
317
|
+
let combinedBody = body;
|
|
318
|
+
if (isGroup && historyKey) {
|
|
319
|
+
log(`${tag}: [STEP 6b 群聊历史] 开始构建, historyKey=${historyKey} bodyLen=${body.length}`);
|
|
320
|
+
combinedBody = buildGroupHistoryContext({
|
|
321
|
+
historyKey,
|
|
322
|
+
currentMessage: body,
|
|
323
|
+
formatEntry: (entry) =>
|
|
324
|
+
core.channel.reply.formatAgentEnvelope({
|
|
325
|
+
channel: "Palz",
|
|
326
|
+
from: entry.sender,
|
|
327
|
+
timestamp: new Date(entry.timestamp),
|
|
328
|
+
body: entry.body,
|
|
329
|
+
envelope: envelopeOptions,
|
|
330
|
+
}),
|
|
331
|
+
log,
|
|
332
|
+
});
|
|
333
|
+
log(`${tag}: [STEP 6b 群聊历史] 构建完成, historyKey=${historyKey} bodyLen=${body.length} combinedBodyLen=${combinedBody.length} hasHistory=${combinedBody.length !== body.length}`);
|
|
334
|
+
// log(`${tag}: [STEP 6b 群聊历史] combinedBody=\n${combinedBody}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 构建 InboundHistory(结构化历史数据,Runtime 会注入到系统提示中)
|
|
338
|
+
const inboundHistory =
|
|
339
|
+
isGroup && historyKey
|
|
340
|
+
? (chatHistories.get(historyKey) ?? []).map((entry) => ({
|
|
341
|
+
sender: entry.sender,
|
|
342
|
+
body: entry.body,
|
|
343
|
+
timestamp: entry.timestamp,
|
|
344
|
+
}))
|
|
345
|
+
: undefined;
|
|
346
|
+
log(`${tag}: [STEP 6b InboundHistory] count=${inboundHistory?.length ?? 0}`);
|
|
347
|
+
|
|
192
348
|
const rawCtx = {
|
|
193
|
-
Body: `(envelope, len=${
|
|
349
|
+
Body: `(envelope, len=${combinedBody.length})`,
|
|
194
350
|
BodyForAgent: messageBody.slice(0, 100),
|
|
351
|
+
InboundHistory: inboundHistory ? `(${inboundHistory.length} entries)` : undefined,
|
|
195
352
|
RawBody: plainText.slice(0, 100),
|
|
196
353
|
CommandBody: plainText.slice(0, 100),
|
|
197
354
|
From: palzFrom,
|
|
198
355
|
To: palzTo,
|
|
199
356
|
SessionKey: route.sessionKey,
|
|
200
357
|
AccountId: route.accountId,
|
|
201
|
-
ChatType:
|
|
358
|
+
ChatType: chatType,
|
|
359
|
+
GroupSubject: isGroup ? msg.conversation_id : undefined,
|
|
202
360
|
SenderId: msg.sender_id,
|
|
203
|
-
SenderName:
|
|
361
|
+
SenderName: senderName,
|
|
204
362
|
Provider: "palz-connector",
|
|
205
363
|
Surface: "palz-connector",
|
|
206
364
|
MessageSid: msg.msg_id,
|
|
207
365
|
Timestamp: Date.now(),
|
|
208
|
-
WasMentioned:
|
|
366
|
+
WasMentioned: wasMentioned,
|
|
209
367
|
CommandAuthorized: commandAuthorized,
|
|
210
368
|
OriginatingChannel: "palz-connector",
|
|
211
369
|
OriginatingTo: palzTo,
|
|
@@ -214,22 +372,24 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
214
372
|
log(`${tag}: [STEP 6b inbound context] 输入: ${JSON.stringify(rawCtx)}`);
|
|
215
373
|
|
|
216
374
|
const ctx = core.channel.reply.finalizeInboundContext({
|
|
217
|
-
Body:
|
|
375
|
+
Body: combinedBody,
|
|
218
376
|
BodyForAgent: messageBody,
|
|
377
|
+
InboundHistory: inboundHistory,
|
|
219
378
|
RawBody: plainText,
|
|
220
379
|
CommandBody: plainText,
|
|
221
380
|
From: palzFrom,
|
|
222
381
|
To: palzTo,
|
|
223
382
|
SessionKey: route.sessionKey,
|
|
224
383
|
AccountId: route.accountId,
|
|
225
|
-
ChatType:
|
|
384
|
+
ChatType: chatType,
|
|
385
|
+
GroupSubject: isGroup ? msg.conversation_id : undefined,
|
|
226
386
|
SenderId: msg.sender_id,
|
|
227
|
-
SenderName:
|
|
387
|
+
SenderName: senderName,
|
|
228
388
|
Provider: "palz-connector",
|
|
229
389
|
Surface: "palz-connector",
|
|
230
390
|
MessageSid: msg.msg_id,
|
|
231
391
|
Timestamp: Date.now(),
|
|
232
|
-
WasMentioned:
|
|
392
|
+
WasMentioned: wasMentioned,
|
|
233
393
|
CommandAuthorized: commandAuthorized,
|
|
234
394
|
OriginatingChannel: "palz-connector",
|
|
235
395
|
OriginatingTo: palzTo,
|
|
@@ -276,6 +436,11 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
276
436
|
}),
|
|
277
437
|
});
|
|
278
438
|
|
|
439
|
+
// AI 回复完成后清空群聊历史(已拼入上下文,避免下次重复)
|
|
440
|
+
if (isGroup && historyKey) {
|
|
441
|
+
clearGroupHistory(historyKey, log);
|
|
442
|
+
}
|
|
443
|
+
|
|
279
444
|
const dispatchElapsedMs = Date.now() - dispatchStartMs;
|
|
280
445
|
log(
|
|
281
446
|
`${tag}: [STEP 6d 输出] queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)} 耗时=${dispatchElapsedMs}ms`,
|
package/src/channel.ts
CHANGED
package/src/dedup.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 消息去重
|
|
3
3
|
*
|
|
4
|
-
* 基于 msg_id 防止重复处理,TTL 5 分钟。
|
|
4
|
+
* 基于 agentId + conversationId + msg_id 防止重复处理,TTL 5 分钟。
|
|
5
|
+
* 同一条消息可以被不同 agent 各自处理一次(同群多 bot 场景)。
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const processedMessages = new Map<string, number>();
|
|
@@ -10,8 +11,8 @@ const MESSAGE_DEDUP_TTL = 5 * 60 * 1000;
|
|
|
10
11
|
function cleanup(): void {
|
|
11
12
|
const before = processedMessages.size;
|
|
12
13
|
const now = Date.now();
|
|
13
|
-
for (const [
|
|
14
|
-
if (now - ts > MESSAGE_DEDUP_TTL) processedMessages.delete(
|
|
14
|
+
for (const [key, ts] of processedMessages) {
|
|
15
|
+
if (now - ts > MESSAGE_DEDUP_TTL) processedMessages.delete(key);
|
|
15
16
|
}
|
|
16
17
|
console.log(`palz-dedup: cleanup ${before} → ${processedMessages.size} entries`);
|
|
17
18
|
}
|
|
@@ -20,11 +21,15 @@ function cleanup(): void {
|
|
|
20
21
|
* 尝试标记消息为已处理。
|
|
21
22
|
* 返回 true 表示此消息之前未处理过(可以继续处理),
|
|
22
23
|
* 返回 false 表示重复消息(应跳过)。
|
|
24
|
+
*
|
|
25
|
+
* agentId 用于同群多 bot 场景:同一条 msg_id 可被不同 agent 各自处理。
|
|
26
|
+
* conversationId 防止不同会话间 msg_id 碰撞导致误去重。
|
|
23
27
|
*/
|
|
24
|
-
export function tryClaimMessage(msgId: string): boolean {
|
|
28
|
+
export function tryClaimMessage(msgId: string, agentId: string = "main", conversationId: string = ""): boolean {
|
|
25
29
|
if (!msgId) return true;
|
|
26
|
-
|
|
27
|
-
processedMessages.
|
|
30
|
+
const key = `${agentId}:${conversationId}:${msgId}`;
|
|
31
|
+
if (processedMessages.has(key)) return false;
|
|
32
|
+
processedMessages.set(key, Date.now());
|
|
28
33
|
if (processedMessages.size >= 200) cleanup();
|
|
29
34
|
return true;
|
|
30
35
|
}
|
package/src/outbound.ts
CHANGED
|
@@ -20,14 +20,15 @@ export const palzOutbound = {
|
|
|
20
20
|
log(`palz-outbound: [sendText] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} text="${(text || "").slice(0, 120)}"`);
|
|
21
21
|
|
|
22
22
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
23
|
-
const { senderId, conversationId } = parsePalzTarget(to);
|
|
24
|
-
log(`palz-outbound: [sendText] 解析: senderId="${senderId}" conversationId="${conversationId}" botId=${account.config.botId}`);
|
|
23
|
+
const { senderId, conversationId, conversationType } = parsePalzTarget(to);
|
|
24
|
+
log(`palz-outbound: [sendText] 解析: senderId="${senderId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
|
|
25
25
|
|
|
26
26
|
const result = await sendToPalzIM({
|
|
27
27
|
config: account.config,
|
|
28
28
|
conversationId,
|
|
29
29
|
content: text,
|
|
30
30
|
senderId,
|
|
31
|
+
conversationType,
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
const output = { channel: "palz-connector", messageId: Date.now().toString() };
|
|
@@ -41,8 +42,8 @@ export const palzOutbound = {
|
|
|
41
42
|
log(`palz-outbound: [sendMedia] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} mediaUrl="${(mediaUrl || "").slice(0, 200)}"`);
|
|
42
43
|
|
|
43
44
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
44
|
-
const { senderId, conversationId } = parsePalzTarget(to);
|
|
45
|
-
log(`palz-outbound: [sendMedia] 解析: senderId="${senderId}" conversationId="${conversationId}" botId=${account.config.botId}`);
|
|
45
|
+
const { senderId, conversationId, conversationType } = parsePalzTarget(to);
|
|
46
|
+
log(`palz-outbound: [sendMedia] 解析: senderId="${senderId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
|
|
46
47
|
|
|
47
48
|
const contentParts: ContentPart[] = [];
|
|
48
49
|
|
|
@@ -73,6 +74,7 @@ export const palzOutbound = {
|
|
|
73
74
|
conversationId,
|
|
74
75
|
content,
|
|
75
76
|
senderId,
|
|
77
|
+
conversationType,
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
const output = { channel: "palz-connector", messageId: Date.now().toString() };
|
package/src/targets.ts
CHANGED
|
@@ -16,18 +16,26 @@ export function looksLikePalzId(raw: string): boolean {
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* 从 "to" 地址中解析出 senderId 和 conversationId。
|
|
19
|
-
* 格式:
|
|
19
|
+
* 格式:
|
|
20
|
+
* DM: "{senderId}:{conversationId}"
|
|
21
|
+
* 群聊: "chat:{conversationId}"(senderId 为空,群消息不需要指定 senderId)
|
|
20
22
|
*/
|
|
21
23
|
export function parsePalzTarget(to: string): {
|
|
22
24
|
senderId?: string;
|
|
23
25
|
conversationId: string;
|
|
26
|
+
conversationType: string;
|
|
24
27
|
} {
|
|
28
|
+
// 群聊目标:chat:xxx → senderId 留空
|
|
29
|
+
if (to.startsWith("chat:")) {
|
|
30
|
+
return { conversationId: to.slice(5), conversationType: "group" };
|
|
31
|
+
}
|
|
25
32
|
const parts = to.split(":");
|
|
26
33
|
if (parts.length >= 2) {
|
|
27
34
|
return {
|
|
28
35
|
senderId: parts[0],
|
|
29
36
|
conversationId: parts.slice(1).join(":"),
|
|
37
|
+
conversationType: "direct",
|
|
30
38
|
};
|
|
31
39
|
}
|
|
32
|
-
return { conversationId: to };
|
|
40
|
+
return { conversationId: to, conversationType: "direct" };
|
|
33
41
|
}
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type OpenAIContent = string | ContentPart[];
|
|
|
17
17
|
export interface PalzMessageEvent {
|
|
18
18
|
event: string;
|
|
19
19
|
sender_id: string;
|
|
20
|
+
sender_name?: string;
|
|
20
21
|
receiver_id: string;
|
|
21
22
|
conversation_id: string;
|
|
22
23
|
conversation_type: string;
|
|
@@ -26,6 +27,8 @@ export interface PalzMessageEvent {
|
|
|
26
27
|
timestamp: number;
|
|
27
28
|
/** 可选,指定 OpenClaw agent ID,覆盖默认路由 */
|
|
28
29
|
agent_id?: string;
|
|
30
|
+
/** 群聊中是否 @了机器人 */
|
|
31
|
+
mentioned_bot?: boolean;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
// ============ 配置 ============
|