palz-connector 1.2.8 → 1.2.9
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/index.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -1
- package/src/bot.ts +159 -28
- package/src/monitor.ts +21 -2
- package/src/send.ts +33 -10
- package/src/tracing.ts +101 -0
- package/src/types.ts +2 -0
package/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - IM 通信协议保持不变(WebSocket 接收消息,HTTP POST 发送回复)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { initTracing } from "./src/tracing.js";
|
|
10
11
|
import { palzPlugin } from "./src/channel.js";
|
|
11
12
|
import { setPalzRuntime } from "./src/runtime.js";
|
|
12
13
|
|
|
@@ -21,6 +22,7 @@ const plugin = {
|
|
|
21
22
|
},
|
|
22
23
|
register(api: any) {
|
|
23
24
|
const log = typeof api.runtime?.log === "function" ? api.runtime.log : console.log;
|
|
25
|
+
initTracing();
|
|
24
26
|
log("palz-connector: register() called, saving runtime and registering channel");
|
|
25
27
|
setPalzRuntime(api.runtime);
|
|
26
28
|
api.registerChannel({ plugin: palzPlugin });
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palz-connector",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"@opentelemetry/api": "^1.9.1",
|
|
20
|
+
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
|
|
21
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
22
|
+
"@opentelemetry/sdk-node": "^0.214.0",
|
|
23
|
+
"@opentelemetry/sdk-trace-node": "^2.6.1",
|
|
24
|
+
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
19
25
|
"ali-oss": "^6.21.0",
|
|
20
26
|
"ws": "^8.18.0"
|
|
21
27
|
}
|
package/src/bot.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { resolvePalzAccount } from "./config.js";
|
|
|
15
15
|
import { tryClaimMessage } from "./dedup.js";
|
|
16
16
|
import { createPalzReplyDispatcher } from "./reply-dispatcher.js";
|
|
17
17
|
import { resolvePalzMediaList } from "./media.js";
|
|
18
|
+
import { tracer, trace, context, SpanStatusCode } from "./tracing.js";
|
|
18
19
|
import type { PalzMessageEvent, OpenAIContent, ContentPart, TextContentPart, PalzMediaInfo } from "./types.js";
|
|
19
20
|
|
|
20
21
|
// ============ group_id 解析 ============
|
|
@@ -173,6 +174,28 @@ export interface HandlePalzMessageParams {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
export async function handlePalzMessage(params: HandlePalzMessageParams): Promise<void> {
|
|
177
|
+
const { msg } = params;
|
|
178
|
+
|
|
179
|
+
// 在当前 trace context(由 monitor.ts ws_recv span 建立)下创建子 span
|
|
180
|
+
return tracer.startActiveSpan("palz.handleMessage", async (span) => {
|
|
181
|
+
span.setAttribute("msg_id", msg.msg_id);
|
|
182
|
+
span.setAttribute("sender_id", msg.sender_id);
|
|
183
|
+
span.setAttribute("conversation_type", msg.conversation_type);
|
|
184
|
+
span.setAttribute("agent_id", msg.agent_id || "main");
|
|
185
|
+
span.setAttribute("is_group", msg.conversation_type === "group");
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await _handlePalzMessageInner(params);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
|
|
191
|
+
throw e;
|
|
192
|
+
} finally {
|
|
193
|
+
span.end();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function _handlePalzMessageInner(params: HandlePalzMessageParams): Promise<void> {
|
|
176
199
|
const { cfg, msg, runtime, accountId } = params;
|
|
177
200
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
178
201
|
const error = typeof runtime?.error === "function" ? runtime.error : console.error;
|
|
@@ -181,11 +204,16 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
|
|
|
181
204
|
const isGroup = msg.conversation_type === "group";
|
|
182
205
|
const effectiveAgentId = msg.agent_id || "main";
|
|
183
206
|
|
|
184
|
-
|
|
207
|
+
const step1Filter = `[STEP 1/6 入站过滤] msg_id=${msg.msg_id} sender=${msg.sender_id} conv=${msg.conversation_id} type=${msg.conversation_type} agent=${effectiveAgentId}`;
|
|
208
|
+
log(`${tag}: ${step1Filter}`);
|
|
209
|
+
const span = trace.getActiveSpan();
|
|
210
|
+
span?.addEvent(step1Filter);
|
|
185
211
|
|
|
186
212
|
const content = msg.content;
|
|
187
213
|
if (!content) {
|
|
188
|
-
|
|
214
|
+
const skip = `[STEP 1 跳过] 原因=content为空`;
|
|
215
|
+
log(`${tag}: ${skip}`);
|
|
216
|
+
span?.addEvent(skip);
|
|
189
217
|
return;
|
|
190
218
|
}
|
|
191
219
|
|
|
@@ -197,10 +225,14 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
|
|
|
197
225
|
? content.filter((p: ContentPart) => p.type === "file").length
|
|
198
226
|
: 0;
|
|
199
227
|
|
|
200
|
-
|
|
228
|
+
const step1Parse = `[STEP 1 解析] plainText="${plainText}" (len=${plainText.length}) hasMedia=${hasMedia} mediaCount=${mediaCount}`;
|
|
229
|
+
log(`${tag}: ${step1Parse}`);
|
|
230
|
+
span?.addEvent(step1Parse);
|
|
201
231
|
|
|
202
232
|
if (!plainText && !hasMedia) {
|
|
203
|
-
|
|
233
|
+
const skip = `[STEP 1 跳过] 原因=无文本且无媒体`;
|
|
234
|
+
log(`${tag}: ${skip}`);
|
|
235
|
+
span?.addEvent(skip);
|
|
204
236
|
return;
|
|
205
237
|
}
|
|
206
238
|
|
|
@@ -215,9 +247,11 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
|
|
|
215
247
|
const historyKey = `${effectiveAgentId}:${msg.conversation_id}`;
|
|
216
248
|
const senderName = msg.sender_name || msg.sender_id;
|
|
217
249
|
log(`${tag}: [STEP 1 群聊历史] 未@机器人, 准备记录历史 historyKey=${historyKey} mentioned_bot=${msg.mentioned_bot} conversation_type=${msg.conversation_type}`);
|
|
250
|
+
span?.addEvent(`[STEP 1 群聊历史] 未@机器人, 准备记录历史 historyKey=${historyKey} mentioned_bot=${msg.mentioned_bot} conversation_type=${msg.conversation_type}`);
|
|
218
251
|
// 解析媒体文件(图片/文档等),缓存到本地
|
|
219
252
|
const historyMediaList = await resolvePalzMediaList(msg.content, log);
|
|
220
253
|
log(`${tag}: [STEP 1 群聊历史] 媒体解析完成: mediaCount=${historyMediaList.length}`);
|
|
254
|
+
span?.addEvent(`[STEP 1 群聊历史] 媒体解析完成: mediaCount=${historyMediaList.length}`);
|
|
221
255
|
const bodyWithPlaceholder = historyMediaList.length > 0
|
|
222
256
|
? `${senderName}: ${plainText} ${historyMediaList.map(m => m.placeholder).join(' ')}`
|
|
223
257
|
: `${senderName}: ${plainText}`;
|
|
@@ -233,36 +267,82 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
|
|
|
233
267
|
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
234
268
|
log,
|
|
235
269
|
});
|
|
236
|
-
|
|
270
|
+
const skipHistory = `[STEP 1 跳过] 原因=群聊中未@机器人, 已记录到历史 historyKey=${historyKey} mediaCount=${historyMediaList.length}`;
|
|
271
|
+
log(`${tag}: ${skipHistory}`);
|
|
272
|
+
span?.addEvent(skipHistory);
|
|
237
273
|
return;
|
|
238
274
|
}
|
|
239
275
|
if (isGroup && !wasMentioned && !groupContextCacheEnabled) {
|
|
240
|
-
|
|
276
|
+
const passThrough = `[STEP 1 群聊直通] groupContextCache=false, 未@消息也将发送给AI mentioned_bot=${msg.mentioned_bot}`;
|
|
277
|
+
log(`${tag}: ${passThrough}`);
|
|
278
|
+
span?.addEvent(passThrough);
|
|
241
279
|
}
|
|
242
280
|
|
|
243
281
|
// 去重(按 agentId + conversationId 隔离,同群多 bot 场景)
|
|
244
282
|
const claimed = tryClaimMessage(msg.msg_id, effectiveAgentId, msg.conversation_id);
|
|
245
|
-
|
|
246
|
-
|
|
283
|
+
const step2 = `[STEP 2/6 去重] msg_id=${msg.msg_id} agent=${effectiveAgentId} conv=${msg.conversation_id} claimed=${claimed}`;
|
|
284
|
+
log(`${tag}: ${step2}`);
|
|
285
|
+
span?.addEvent(step2);
|
|
286
|
+
if (!claimed) {
|
|
287
|
+
span?.addEvent(`[STEP 2 跳过] 原因=消息已被处理(去重)`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
247
290
|
|
|
248
291
|
// 入队(按 agentId 隔离,不同 agent 并行处理)
|
|
249
292
|
const queueKey = isGroup
|
|
250
293
|
? `${effectiveAgentId}:${msg.conversation_id}`
|
|
251
294
|
: `${effectiveAgentId}:${msg.sender_id}:${msg.conversation_id}`;
|
|
252
|
-
|
|
295
|
+
const step3 = `[STEP 3/6 入队] queueKey="${queueKey}" isGroup=${isGroup}`;
|
|
296
|
+
log(`${tag}: ${step3}`);
|
|
297
|
+
span?.addEvent(step3);
|
|
253
298
|
|
|
299
|
+
// 捕获当前 trace context,enqueue 异步回调中恢复
|
|
300
|
+
const capturedCtx = context.active();
|
|
301
|
+
|
|
302
|
+
const enqueuedAt = Date.now();
|
|
254
303
|
enqueue(queueKey, async () => {
|
|
255
304
|
const startMs = Date.now();
|
|
305
|
+
const queueWaitMs = startMs - enqueuedAt;
|
|
306
|
+
if (queueWaitMs > 100) {
|
|
307
|
+
const waitLog = `[队列等待] msg_id=${msg.msg_id} queueKey=${queueKey} waitMs=${queueWaitMs}`;
|
|
308
|
+
log(`${tag}: ${waitLog}`);
|
|
309
|
+
span?.addEvent(waitLog);
|
|
310
|
+
}
|
|
256
311
|
try {
|
|
257
|
-
await
|
|
258
|
-
|
|
312
|
+
await context.with(capturedCtx, () =>
|
|
313
|
+
dispatchPalzMessage({ cfg, msg, runtime, accountId }),
|
|
314
|
+
);
|
|
315
|
+
const doneLog = `[完成] msg_id=${msg.msg_id} 总耗时=${Date.now() - startMs}ms queueWait=${queueWaitMs}ms`;
|
|
316
|
+
log(`${tag}: ${doneLog}`);
|
|
317
|
+
span?.addEvent(doneLog);
|
|
259
318
|
} catch (err) {
|
|
260
|
-
|
|
319
|
+
const failLog = `[失败] msg_id=${msg.msg_id} 总耗时=${Date.now() - startMs}ms queueWait=${queueWaitMs}ms error=${String(err)}`;
|
|
320
|
+
error(`${tag}: ${failLog}`);
|
|
321
|
+
span?.addEvent(failLog);
|
|
261
322
|
}
|
|
262
323
|
});
|
|
263
324
|
}
|
|
264
325
|
|
|
265
326
|
async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<void> {
|
|
327
|
+
const { msg, runtime } = params;
|
|
328
|
+
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
329
|
+
|
|
330
|
+
return tracer.startActiveSpan("palz.dispatch", async (span) => {
|
|
331
|
+
try {
|
|
332
|
+
span.setAttribute("msg_id", msg.msg_id);
|
|
333
|
+
span.setAttribute("conversation_id", msg.conversation_id);
|
|
334
|
+
await _dispatchPalzMessageInner(params);
|
|
335
|
+
} catch (e) {
|
|
336
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
|
|
337
|
+
log(`palz: [dispatch span error] ${String(e)}`);
|
|
338
|
+
throw e;
|
|
339
|
+
} finally {
|
|
340
|
+
span.end();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promise<void> {
|
|
266
346
|
const { cfg, msg, runtime, accountId } = params;
|
|
267
347
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
268
348
|
const tag = `palz[${accountId}]`;
|
|
@@ -287,15 +367,22 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
287
367
|
const mediaCount = Array.isArray(msg.content)
|
|
288
368
|
? msg.content.filter((p: ContentPart) => p.type === "file").length
|
|
289
369
|
: 0;
|
|
290
|
-
|
|
370
|
+
const span = trace.getActiveSpan();
|
|
371
|
+
const step4Input = `[STEP 4/6 媒体解析] 输入: contentType=${typeof msg.content === "string" ? "string" : "array"} mediaCount=${mediaCount}`;
|
|
372
|
+
log(`${tag}: ${step4Input}`);
|
|
373
|
+
span?.addEvent(step4Input);
|
|
291
374
|
const mediaList = await resolvePalzMediaList(msg.content, log);
|
|
292
375
|
let mediaPayload = buildMediaPayload(mediaList);
|
|
293
|
-
|
|
376
|
+
const step4Output = `[STEP 4 输出] mediaList=${JSON.stringify(mediaList.map((m) => ({ path: m.path, contentType: m.contentType })))} mediaPayload=${JSON.stringify(mediaPayload)}`;
|
|
377
|
+
log(`${tag}: ${step4Output}`);
|
|
378
|
+
span?.addEvent(step4Output);
|
|
294
379
|
|
|
295
380
|
// STEP 5: 解析路由
|
|
296
381
|
const peerKind = isGroup ? "group" : "direct";
|
|
297
382
|
const routeInput = { cfg: "(cfg)", channel: "palz-connector", accountId, peer: { kind: peerKind, id: peerId } };
|
|
298
|
-
|
|
383
|
+
const step5Input = `[STEP 5/6 路由解析] 输入: ${JSON.stringify(routeInput)} agent_id=${msg.agent_id ?? "(auto)"}`;
|
|
384
|
+
log(`${tag}: ${step5Input}`);
|
|
385
|
+
span?.addEvent(step5Input);
|
|
299
386
|
const route = core.channel.routing.resolveAgentRoute({
|
|
300
387
|
cfg,
|
|
301
388
|
channel: "palz-connector",
|
|
@@ -309,17 +396,23 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
309
396
|
const oldAgentId = route.agentId;
|
|
310
397
|
route.agentId = effectiveAgentId;
|
|
311
398
|
route.sessionKey = route.sessionKey.replace(`agent:${oldAgentId}:`, `agent:${effectiveAgentId}:`);
|
|
312
|
-
|
|
399
|
+
const step5Override = `[STEP 5 覆盖] agentId: ${oldAgentId} -> ${effectiveAgentId}, sessionKey=${route.sessionKey} (${msg.agent_id ? "IM指定" : "默认main"})`;
|
|
400
|
+
log(`${tag}: ${step5Override}`);
|
|
401
|
+
span?.addEvent(step5Override);
|
|
313
402
|
}
|
|
314
403
|
|
|
315
|
-
|
|
404
|
+
const step5Output = `[STEP 5 输出] agentId=${route.agentId} sessionKey=${route.sessionKey} accountId=${route.accountId} matchedBy=${route.matchedBy}`;
|
|
405
|
+
log(`${tag}: ${step5Output}`);
|
|
406
|
+
span?.addEvent(step5Output);
|
|
316
407
|
|
|
317
408
|
// STEP 6a: 构建 envelope body
|
|
318
409
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
319
410
|
const messageBody = `${senderName}: ${plainText}`;
|
|
320
411
|
// 群聊 from 带上 conversation_id 以区分不同用户
|
|
321
412
|
const envelopeFrom = isGroup ? `${msg.conversation_id}:${msg.sender_id}` : msg.sender_id;
|
|
322
|
-
|
|
413
|
+
const step6aInput = `[STEP 6a/6 envelope构建] 输入: channel=Palz from=${envelopeFrom} messageBody="${messageBody.slice(0, 120)}" envelopeOptions=${JSON.stringify(envelopeOptions)}`;
|
|
414
|
+
log(`${tag}: ${step6aInput}`);
|
|
415
|
+
span?.addEvent(step6aInput);
|
|
323
416
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
324
417
|
channel: "Palz",
|
|
325
418
|
from: envelopeFrom,
|
|
@@ -327,7 +420,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
327
420
|
body: messageBody,
|
|
328
421
|
envelope: envelopeOptions,
|
|
329
422
|
});
|
|
330
|
-
|
|
423
|
+
const step6aOutput = `[STEP 6a 输出] envelope body (len=${body.length}): "${body.slice(0, 200)}"`;
|
|
424
|
+
log(`${tag}: ${step6aOutput}`);
|
|
425
|
+
span?.addEvent(step6aOutput);
|
|
331
426
|
|
|
332
427
|
// STEP 6b: 构建 inbound context
|
|
333
428
|
const palzFrom = `palz:${msg.sender_id}`;
|
|
@@ -343,7 +438,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
343
438
|
authorizers: [{ configured: false, allowed: true }],
|
|
344
439
|
})
|
|
345
440
|
: undefined;
|
|
346
|
-
|
|
441
|
+
const step6bAuth = `[STEP 6b 命令授权] needsCommandAuth=${needsCommandAuth} commandAuthorized=${commandAuthorized}`;
|
|
442
|
+
log(`${tag}: ${step6bAuth}`);
|
|
443
|
+
span?.addEvent(step6bAuth);
|
|
347
444
|
|
|
348
445
|
const chatType = isGroup ? "group" : "direct";
|
|
349
446
|
|
|
@@ -388,7 +485,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
388
485
|
timestamp: entry.timestamp,
|
|
389
486
|
}))
|
|
390
487
|
: undefined;
|
|
391
|
-
|
|
488
|
+
const step6bHistory = `[STEP 6b InboundHistory] count=${inboundHistory?.length ?? 0}`;
|
|
489
|
+
log(`${tag}: ${step6bHistory}`);
|
|
490
|
+
span?.addEvent(step6bHistory);
|
|
392
491
|
|
|
393
492
|
// 构建 UntrustedContext:将 IM 消息的关键字段注入到 AI agent 的上下文中
|
|
394
493
|
const untrustedContext: string[] = [
|
|
@@ -407,7 +506,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
407
506
|
if (msg.owner_name) {
|
|
408
507
|
untrustedContext.push(`owner_name: ${msg.owner_name}`);
|
|
409
508
|
}
|
|
410
|
-
|
|
509
|
+
const step6bCtx = `[STEP 6b UntrustedContext] entries=${untrustedContext.length} values=${JSON.stringify(untrustedContext)}`;
|
|
510
|
+
log(`${tag}: ${step6bCtx}`);
|
|
511
|
+
span?.addEvent(step6bCtx);
|
|
411
512
|
|
|
412
513
|
const ctx = core.channel.reply.finalizeInboundContext({
|
|
413
514
|
Body: combinedBody,
|
|
@@ -434,7 +535,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
434
535
|
OriginatingTo: palzTo,
|
|
435
536
|
...mediaPayload,
|
|
436
537
|
});
|
|
437
|
-
|
|
538
|
+
const step6bOutput = `[STEP 6b 输出] finalized context keys=[${Object.keys(ctx).join(",")}] CommandAuthorized=${ctx.CommandAuthorized}`;
|
|
539
|
+
log(`${tag}: ${step6bOutput}`);
|
|
540
|
+
span?.addEvent(step6bOutput);
|
|
438
541
|
ctx.metadata = {
|
|
439
542
|
...ctx.metadata,
|
|
440
543
|
traceId: msg.msg_id,
|
|
@@ -449,7 +552,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
449
552
|
enableStreaming: useStream,
|
|
450
553
|
msgId: msg.msg_id,
|
|
451
554
|
};
|
|
452
|
-
|
|
555
|
+
const step6c = `[STEP 6c 创建dispatcher] 输入: ${JSON.stringify(dispatcherParams)}`;
|
|
556
|
+
log(`${tag}: ${step6c}`);
|
|
557
|
+
span?.addEvent(step6c);
|
|
453
558
|
|
|
454
559
|
const { dispatcher, replyOptions, markDispatchIdle } = createPalzReplyDispatcher({
|
|
455
560
|
cfg,
|
|
@@ -468,7 +573,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
468
573
|
// STEP 6d: 分发消息给 AI
|
|
469
574
|
// channel registry 守卫已在 index.ts 中通过 defineProperty 安装,
|
|
470
575
|
// 每次读取 state.registry 时会自动注入 palz-connector channel。
|
|
471
|
-
|
|
576
|
+
const step6dStart = `[STEP 6d AI分发] 开始 session=${route.sessionKey} stream=${useStream}`;
|
|
577
|
+
log(`${tag}: ${step6dStart}`);
|
|
578
|
+
span?.addEvent(step6dStart);
|
|
472
579
|
const dispatchStartMs = Date.now();
|
|
473
580
|
|
|
474
581
|
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
|
@@ -489,7 +596,31 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
489
596
|
}
|
|
490
597
|
|
|
491
598
|
const dispatchElapsedMs = Date.now() - dispatchStartMs;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
);
|
|
599
|
+
const step6dOutput = `[STEP 6d 输出] queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)} 耗时=${dispatchElapsedMs}ms`;
|
|
600
|
+
log(`${tag}: ${step6dOutput}`);
|
|
601
|
+
span?.addEvent(step6dOutput);
|
|
602
|
+
span?.setAttribute("dispatch_elapsed_ms", dispatchElapsedMs);
|
|
603
|
+
if (!queuedFinal && (!counts || Object.keys(counts).length === 0)) {
|
|
604
|
+
const noReply = `[STEP 6d 警告] AI 未产生回复 session=${route.sessionKey}`;
|
|
605
|
+
log(`${tag}: ${noReply}`);
|
|
606
|
+
span?.addEvent(noReply);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// 解析 sessionFile 路径,写入 trace
|
|
610
|
+
try {
|
|
611
|
+
const agentId = route.agentId;
|
|
612
|
+
const storePath = core.agent.session.resolveStorePath(undefined, { agentId });
|
|
613
|
+
const store = core.agent.session.loadSessionStore(storePath, { skipCache: true });
|
|
614
|
+
const normalizedKey = route.sessionKey.trim().toLowerCase();
|
|
615
|
+
const entry = store[normalizedKey] ?? store[route.sessionKey];
|
|
616
|
+
if (entry?.sessionId) {
|
|
617
|
+
const sessionFile = core.agent.session.resolveSessionFilePath(entry.sessionId, entry, { agentId });
|
|
618
|
+
const traceLog = `[TRACE] sessionFile=${sessionFile} sessionKey=${route.sessionKey} agentId=${agentId}`;
|
|
619
|
+
log(`${tag}: ${traceLog}`);
|
|
620
|
+
span?.setAttribute("session_file", sessionFile);
|
|
621
|
+
span?.addEvent(traceLog);
|
|
622
|
+
}
|
|
623
|
+
} catch (err) {
|
|
624
|
+
log(`${tag}: [TRACE] 解析 sessionFile 失败: ${String(err)}`);
|
|
625
|
+
}
|
|
495
626
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import WebSocket from "ws";
|
|
9
9
|
import { handlePalzMessage } from "./bot.js";
|
|
10
|
+
import { tracer, extractTraceparentContext, SpanStatusCode } from "./tracing.js";
|
|
10
11
|
import type { PalzConfig, PalzMessageEvent } from "./types.js";
|
|
11
12
|
|
|
12
13
|
export interface MonitorPalzParams {
|
|
@@ -97,8 +98,26 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
|
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
100
|
messageCount++;
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
|
|
102
|
+
// 从 IM 上游的 traceparent 恢复 trace context
|
|
103
|
+
const parentCtx = extractTraceparentContext(msg.traceparent);
|
|
104
|
+
|
|
105
|
+
tracer.startActiveSpan("palz.ws_recv", {}, parentCtx, (span) => {
|
|
106
|
+
try {
|
|
107
|
+
span.setAttribute("raw_message", raw);
|
|
108
|
+
|
|
109
|
+
log(`palz[${accountId}]: [WS_RECV] #${messageCount} full_message=${raw.slice(0, 1000)} traceId=${span.spanContext().traceId}`);
|
|
110
|
+
|
|
111
|
+
handlePalzMessage({ cfg, msg, runtime, accountId }).catch((err: any) => {
|
|
112
|
+
error(`palz[${accountId}]: [WS_RECV] handlePalzMessage unhandled error: ${err.message ?? err}`);
|
|
113
|
+
});
|
|
114
|
+
} catch (e) {
|
|
115
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
|
|
116
|
+
throw e;
|
|
117
|
+
} finally {
|
|
118
|
+
span.end();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
102
121
|
} catch (err: any) {
|
|
103
122
|
error(`palz[${accountId}]: [WS_RECV] parse error: ${err.message}, raw=${raw.slice(0, 500)}`);
|
|
104
123
|
}
|
package/src/send.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 支持一次性发送和流式发送两种模式。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { tracer, trace, SpanStatusCode, buildTraceparentHeader } from "./tracing.js";
|
|
8
9
|
import type { SendToIMParams, OpenAIContent } from "./types.js";
|
|
9
10
|
|
|
10
11
|
let msgSeq = 0;
|
|
@@ -14,9 +15,24 @@ function nextMsgId(): string {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function sendToPalzIM(params: SendToIMParams): Promise<any> {
|
|
18
|
+
return tracer.startActiveSpan("palz.sendToIM", async (span) => {
|
|
19
|
+
try {
|
|
20
|
+
const result = await _sendToPalzIMInner(params);
|
|
21
|
+
return result;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
|
|
24
|
+
throw e;
|
|
25
|
+
} finally {
|
|
26
|
+
span.end();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function _sendToPalzIMInner(params: SendToIMParams): Promise<any> {
|
|
17
32
|
const { config, conversationId, content, conversationType, msgId, senderId, stream, msgType, groupId } = params;
|
|
18
33
|
const url = `${config.apiBaseUrl}/bot/send`;
|
|
19
34
|
const resolvedMsgId = msgId || nextMsgId();
|
|
35
|
+
const span = trace.getActiveSpan();
|
|
20
36
|
|
|
21
37
|
const reqBody: Record<string, unknown> = {
|
|
22
38
|
bot_id: config.botId,
|
|
@@ -46,14 +62,21 @@ export async function sendToPalzIM(params: SendToIMParams): Promise<any> {
|
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
const reqBodyStr = JSON.stringify(reqBody);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
65
|
+
const reqLog = `[HTTP_REQ] POST ${url} body_length=${reqBodyStr.length}\n request_body=${reqBodyStr}`;
|
|
66
|
+
console.log(`palz-send: ${reqLog}`);
|
|
67
|
+
span?.addEvent(reqLog);
|
|
68
|
+
|
|
69
|
+
// 构建请求 headers,注入 Traceparent
|
|
70
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
71
|
+
const traceparent = buildTraceparentHeader();
|
|
72
|
+
if (traceparent) {
|
|
73
|
+
headers["Traceparent"] = traceparent;
|
|
74
|
+
}
|
|
52
75
|
|
|
53
76
|
const startMs = Date.now();
|
|
54
77
|
const response = await fetch(url, {
|
|
55
78
|
method: "POST",
|
|
56
|
-
headers
|
|
79
|
+
headers,
|
|
57
80
|
body: reqBodyStr,
|
|
58
81
|
});
|
|
59
82
|
const elapsedMs = Date.now() - startMs;
|
|
@@ -65,15 +88,15 @@ export async function sendToPalzIM(params: SendToIMParams): Promise<any> {
|
|
|
65
88
|
} catch {}
|
|
66
89
|
|
|
67
90
|
if (!response.ok) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
);
|
|
91
|
+
const failLog = `[HTTP_RES] FAILED status=${response.status} elapsed=${elapsedMs}ms\n response_headers=${JSON.stringify(Object.fromEntries(response.headers.entries()))}\n response_body=${rawText.slice(0, 500)}`;
|
|
92
|
+
console.error(`palz-send: ${failLog}`);
|
|
93
|
+
span?.addEvent(failLog);
|
|
71
94
|
throw new Error(`Palz send failed: ${response.status} ${rawText}`);
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
97
|
+
const okLog = `[HTTP_RES] OK status=${response.status} elapsed=${elapsedMs}ms msg_id=${resolvedMsgId}${traceparent ? ` Traceparent=${traceparent}` : ""}\n response_body=${rawText.slice(0, 500)}`;
|
|
98
|
+
console.log(`palz-send: ${okLog}`);
|
|
99
|
+
span?.addEvent(okLog);
|
|
77
100
|
return body;
|
|
78
101
|
}
|
|
79
102
|
|
package/src/tracing.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Tracing 初始化 + 工具函数
|
|
3
|
+
*
|
|
4
|
+
* 接入阿里云 ARMS 分布式链路追踪。
|
|
5
|
+
* - initTracing(): 初始化 SDK,在 index.ts register() 最前面调用
|
|
6
|
+
* - tracer: 全局 tracer 实例
|
|
7
|
+
* - extractTraceparentContext(): 从 W3C traceparent 字符串恢复上游 context
|
|
8
|
+
* - buildTraceparentHeader(): 构建当前 span 的 W3C traceparent 字符串
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
12
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
13
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
14
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
15
|
+
import {
|
|
16
|
+
trace,
|
|
17
|
+
context,
|
|
18
|
+
SpanContext,
|
|
19
|
+
TraceFlags,
|
|
20
|
+
ROOT_CONTEXT,
|
|
21
|
+
SpanStatusCode,
|
|
22
|
+
} from "@opentelemetry/api";
|
|
23
|
+
|
|
24
|
+
export { trace, context, SpanStatusCode, ROOT_CONTEXT };
|
|
25
|
+
export type { SpanContext };
|
|
26
|
+
|
|
27
|
+
// 阿里云 ARMS 接入点
|
|
28
|
+
const ARMS_ENDPOINT =
|
|
29
|
+
"http://tracing-analysis-dc-bj.aliyuncs.com/adapt_cd5l20bzv2@591056c1ce22f09_cd5l20bzv2@53df7ad2afe8301/api/otlp/traces";
|
|
30
|
+
|
|
31
|
+
const helmEnv = process.env.HELM_ENV || "local";
|
|
32
|
+
const serviceName = `palz-connector-${helmEnv}`;
|
|
33
|
+
|
|
34
|
+
let sdk: NodeSDK | null = null;
|
|
35
|
+
|
|
36
|
+
export function initTracing(): void {
|
|
37
|
+
if (sdk) return;
|
|
38
|
+
|
|
39
|
+
sdk = new NodeSDK({
|
|
40
|
+
resource: resourceFromAttributes({
|
|
41
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
42
|
+
}),
|
|
43
|
+
traceExporter: new OTLPTraceExporter({
|
|
44
|
+
url: ARMS_ENDPOINT,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
sdk.start();
|
|
49
|
+
console.log(
|
|
50
|
+
`[tracing] initialized: service=${serviceName} endpoint=${ARMS_ENDPOINT}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const tracer = trace.getTracer("palz-connector");
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 解析 W3C traceparent 字符串,返回携带上游 span context 的 OTel Context。
|
|
58
|
+
*
|
|
59
|
+
* traceparent 格式: {version}-{traceId}-{spanId}-{traceFlags}
|
|
60
|
+
* 例: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
|
|
61
|
+
*/
|
|
62
|
+
const TRACEPARENT_RE =
|
|
63
|
+
/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
64
|
+
|
|
65
|
+
export function extractTraceparentContext(
|
|
66
|
+
traceparent: string | undefined,
|
|
67
|
+
): typeof ROOT_CONTEXT {
|
|
68
|
+
if (!traceparent) return ROOT_CONTEXT;
|
|
69
|
+
|
|
70
|
+
const match = TRACEPARENT_RE.exec(traceparent.trim());
|
|
71
|
+
if (!match) {
|
|
72
|
+
console.warn(`[tracing] invalid traceparent: ${traceparent}`);
|
|
73
|
+
return ROOT_CONTEXT;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const spanContext: SpanContext = {
|
|
77
|
+
traceId: match[2],
|
|
78
|
+
spanId: match[3],
|
|
79
|
+
traceFlags: parseInt(match[4], 16) as TraceFlags,
|
|
80
|
+
isRemote: true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return trace.setSpanContext(ROOT_CONTEXT, spanContext);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 构建当前活跃 span 的 W3C traceparent 字符串。
|
|
88
|
+
* 用于在 HTTP 回复 header 中注入 Traceparent。
|
|
89
|
+
*/
|
|
90
|
+
export function buildTraceparentHeader(): string | undefined {
|
|
91
|
+
const span = trace.getActiveSpan();
|
|
92
|
+
if (!span) return undefined;
|
|
93
|
+
|
|
94
|
+
const ctx = span.spanContext();
|
|
95
|
+
if (!ctx.traceId || ctx.traceId === "00000000000000000000000000000000") {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, "0");
|
|
100
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
101
|
+
}
|
package/src/types.ts
CHANGED