palz-connector 1.2.8 → 1.3.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/index.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -1
- package/src/bot.ts +172 -29
- package/src/media.ts +38 -1
- package/src/monitor.ts +21 -2
- package/src/oss.ts +3 -2
- package/src/outbound.ts +2 -2
- package/src/reply-dispatcher.ts +3 -1
- 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.
|
|
3
|
+
"version": "1.3.0",
|
|
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
|
@@ -14,7 +14,8 @@ import { getPalzRuntime } from "./runtime.js";
|
|
|
14
14
|
import { resolvePalzAccount } from "./config.js";
|
|
15
15
|
import { tryClaimMessage } from "./dedup.js";
|
|
16
16
|
import { createPalzReplyDispatcher } from "./reply-dispatcher.js";
|
|
17
|
-
import { resolvePalzMediaList } from "./media.js";
|
|
17
|
+
import { resolvePalzMediaList, resolveMediaLocalRoots } 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,
|
|
@@ -432,9 +533,22 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
432
533
|
CommandAuthorized: commandAuthorized,
|
|
433
534
|
OriginatingChannel: "palz-connector",
|
|
434
535
|
OriginatingTo: palzTo,
|
|
536
|
+
GroupSystemPrompt: [
|
|
537
|
+
"## File Sharing",
|
|
538
|
+
"When you create or generate a file that the user requested (documents, images, presentations, spreadsheets, archives, etc.), you MUST include a MEDIA: token in your reply so the file gets delivered to the user.",
|
|
539
|
+
"Always write your normal text reply first (description, summary, or explanation), then append the MEDIA: line(s) at the end.",
|
|
540
|
+
"Format: place `MEDIA: <absolute-file-path>` on its own line at the end of your reply.",
|
|
541
|
+
"Example:",
|
|
542
|
+
" Here is the presentation you requested. It includes 5 slides covering the main topics.",
|
|
543
|
+
" MEDIA: /path/to/workspace/output.pptx",
|
|
544
|
+
"Without the MEDIA: line, the user only sees text and cannot download the file.",
|
|
545
|
+
"Multiple files: one MEDIA: line per file.",
|
|
546
|
+
].join("\n"),
|
|
435
547
|
...mediaPayload,
|
|
436
548
|
});
|
|
437
|
-
|
|
549
|
+
const step6bOutput = `[STEP 6b 输出] finalized context keys=[${Object.keys(ctx).join(",")}] CommandAuthorized=${ctx.CommandAuthorized}`;
|
|
550
|
+
log(`${tag}: ${step6bOutput}`);
|
|
551
|
+
span?.addEvent(step6bOutput);
|
|
438
552
|
ctx.metadata = {
|
|
439
553
|
...ctx.metadata,
|
|
440
554
|
traceId: msg.msg_id,
|
|
@@ -449,7 +563,9 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
449
563
|
enableStreaming: useStream,
|
|
450
564
|
msgId: msg.msg_id,
|
|
451
565
|
};
|
|
452
|
-
|
|
566
|
+
const step6c = `[STEP 6c 创建dispatcher] 输入: ${JSON.stringify(dispatcherParams)}`;
|
|
567
|
+
log(`${tag}: ${step6c}`);
|
|
568
|
+
span?.addEvent(step6c);
|
|
453
569
|
|
|
454
570
|
const { dispatcher, replyOptions, markDispatchIdle } = createPalzReplyDispatcher({
|
|
455
571
|
cfg,
|
|
@@ -463,12 +579,15 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
463
579
|
msgId: msg.msg_id,
|
|
464
580
|
msgType: msg.msg_type,
|
|
465
581
|
groupId,
|
|
582
|
+
mediaLocalRoots: resolveMediaLocalRoots(effectiveAgentId),
|
|
466
583
|
});
|
|
467
584
|
|
|
468
585
|
// STEP 6d: 分发消息给 AI
|
|
469
586
|
// channel registry 守卫已在 index.ts 中通过 defineProperty 安装,
|
|
470
587
|
// 每次读取 state.registry 时会自动注入 palz-connector channel。
|
|
471
|
-
|
|
588
|
+
const step6dStart = `[STEP 6d AI分发] 开始 session=${route.sessionKey} stream=${useStream}`;
|
|
589
|
+
log(`${tag}: ${step6dStart}`);
|
|
590
|
+
span?.addEvent(step6dStart);
|
|
472
591
|
const dispatchStartMs = Date.now();
|
|
473
592
|
|
|
474
593
|
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
|
@@ -489,7 +608,31 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
|
|
|
489
608
|
}
|
|
490
609
|
|
|
491
610
|
const dispatchElapsedMs = Date.now() - dispatchStartMs;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
);
|
|
611
|
+
const step6dOutput = `[STEP 6d 输出] queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)} 耗时=${dispatchElapsedMs}ms`;
|
|
612
|
+
log(`${tag}: ${step6dOutput}`);
|
|
613
|
+
span?.addEvent(step6dOutput);
|
|
614
|
+
span?.setAttribute("dispatch_elapsed_ms", dispatchElapsedMs);
|
|
615
|
+
if (!queuedFinal && (!counts || Object.keys(counts).length === 0)) {
|
|
616
|
+
const noReply = `[STEP 6d 警告] AI 未产生回复 session=${route.sessionKey}`;
|
|
617
|
+
log(`${tag}: ${noReply}`);
|
|
618
|
+
span?.addEvent(noReply);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 解析 sessionFile 路径,写入 trace
|
|
622
|
+
try {
|
|
623
|
+
const agentId = route.agentId;
|
|
624
|
+
const storePath = core.agent.session.resolveStorePath(undefined, { agentId });
|
|
625
|
+
const store = core.agent.session.loadSessionStore(storePath, { skipCache: true });
|
|
626
|
+
const normalizedKey = route.sessionKey.trim().toLowerCase();
|
|
627
|
+
const entry = store[normalizedKey] ?? store[route.sessionKey];
|
|
628
|
+
if (entry?.sessionId) {
|
|
629
|
+
const sessionFile = core.agent.session.resolveSessionFilePath(entry.sessionId, entry, { agentId });
|
|
630
|
+
const traceLog = `[TRACE] sessionFile=${sessionFile} sessionKey=${route.sessionKey} agentId=${agentId}`;
|
|
631
|
+
log(`${tag}: ${traceLog}`);
|
|
632
|
+
span?.setAttribute("session_file", sessionFile);
|
|
633
|
+
span?.addEvent(traceLog);
|
|
634
|
+
}
|
|
635
|
+
} catch (err) {
|
|
636
|
+
log(`${tag}: [TRACE] 解析 sessionFile 失败: ${String(err)}`);
|
|
637
|
+
}
|
|
495
638
|
}
|
package/src/media.ts
CHANGED
|
@@ -18,6 +18,23 @@ import { uploadFileToOss, uploadBufferToOss } from "./oss.js";
|
|
|
18
18
|
|
|
19
19
|
/** OpenClaw 允许访问的媒体目录 */
|
|
20
20
|
const MEDIA_DIR = path.join(os.homedir(), ".openclaw", "media");
|
|
21
|
+
const STATE_DIR = path.join(os.homedir(), ".openclaw");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 根据 agentId 构造媒体文件搜索根路径列表,
|
|
25
|
+
* 与 OpenClaw 的 getAgentScopedMediaLocalRoots 逻辑对齐。
|
|
26
|
+
*/
|
|
27
|
+
export function resolveMediaLocalRoots(agentId?: string): string[] {
|
|
28
|
+
const roots = [MEDIA_DIR, path.join(STATE_DIR, "workspace"), path.join(STATE_DIR, "sandboxes")];
|
|
29
|
+
if (agentId?.trim()) {
|
|
30
|
+
// workspace-{agentId} 形式(非 default agent,或 main 作为非 default 时)
|
|
31
|
+
const workspaceDirById = path.join(STATE_DIR, `workspace-${agentId}`);
|
|
32
|
+
if (!roots.includes(workspaceDirById)) {
|
|
33
|
+
roots.push(workspaceDirById);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return roots;
|
|
37
|
+
}
|
|
21
38
|
|
|
22
39
|
function saveBufferToMediaDir(
|
|
23
40
|
buffer: Buffer,
|
|
@@ -205,11 +222,13 @@ export async function resolvePalzMediaList(
|
|
|
205
222
|
export async function loadMediaAsOssUrl(
|
|
206
223
|
mediaUrl: string,
|
|
207
224
|
log?: (...args: any[]) => void,
|
|
225
|
+
localRoots?: readonly string[],
|
|
208
226
|
): Promise<string | null> {
|
|
209
227
|
log?.(`palz-media: [loadAsOssUrl] 输入: url=${mediaUrl.slice(0, 200)}`);
|
|
210
228
|
|
|
211
229
|
if (
|
|
212
230
|
mediaUrl.startsWith("https://oss.csaiagent.com/") ||
|
|
231
|
+
mediaUrl.startsWith("https://claw.csaiagent.com") ||
|
|
213
232
|
mediaUrl.startsWith("https://cstv-data.oss-cn-beijing.aliyuncs.com/")
|
|
214
233
|
) {
|
|
215
234
|
log?.(`palz-media: [loadAsOssUrl] 已是OSS链接, 直接返回`);
|
|
@@ -241,7 +260,25 @@ export async function loadMediaAsOssUrl(
|
|
|
241
260
|
|
|
242
261
|
// 本地文件路径(绝对或相对)→ 上传到 OSS
|
|
243
262
|
const rawPath = mediaUrl.replace(/^MEDIA:/, "");
|
|
244
|
-
let filePath = path.isAbsolute(rawPath) ? rawPath :
|
|
263
|
+
let filePath = path.isAbsolute(rawPath) ? rawPath : "";
|
|
264
|
+
|
|
265
|
+
// 相对路径:在 localRoots 中逐个查找
|
|
266
|
+
if (!filePath && localRoots && localRoots.length > 0) {
|
|
267
|
+
for (const root of localRoots) {
|
|
268
|
+
const candidate = path.resolve(root, rawPath);
|
|
269
|
+
if (fs.existsSync(candidate)) {
|
|
270
|
+
log?.(`palz-media: [loadAsOssUrl] 在localRoot找到: root=${root} → ${candidate}`);
|
|
271
|
+
filePath = candidate;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 兜底:cwd 解析
|
|
278
|
+
if (!filePath) {
|
|
279
|
+
filePath = path.resolve(rawPath);
|
|
280
|
+
}
|
|
281
|
+
|
|
245
282
|
if (!fs.existsSync(filePath)) {
|
|
246
283
|
const fallback = path.join(MEDIA_DIR, path.basename(filePath));
|
|
247
284
|
if (fs.existsSync(fallback)) {
|
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/oss.ts
CHANGED
|
@@ -52,12 +52,13 @@ export async function uploadFileToOss(
|
|
|
52
52
|
try {
|
|
53
53
|
const client = getOssClient();
|
|
54
54
|
const ext = path.extname(localPath) || ".png";
|
|
55
|
-
const
|
|
55
|
+
const baseName = path.basename(localPath, ext);
|
|
56
|
+
const objectName = `${OSS_UPLOAD_PREFIX}/${baseName}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
|
|
56
57
|
|
|
57
58
|
const fileBuffer = fs.readFileSync(localPath);
|
|
58
59
|
await client.put(objectName, fileBuffer);
|
|
59
60
|
|
|
60
|
-
const fileUrl = `https://${OSS_CUSTOM_DOMAIN}/${objectName}`;
|
|
61
|
+
const fileUrl = `https://${OSS_CUSTOM_DOMAIN}/${objectName.split('/').map(s => encodeURIComponent(s)).join('/')}`;
|
|
61
62
|
log?.(`palz-oss: [upload] 成功: objectName=${objectName} url=${fileUrl}`);
|
|
62
63
|
return fileUrl;
|
|
63
64
|
} catch (err: any) {
|
package/src/outbound.ts
CHANGED
|
@@ -37,7 +37,7 @@ export const palzOutbound = {
|
|
|
37
37
|
},
|
|
38
38
|
|
|
39
39
|
sendMedia: async (ctx: any) => {
|
|
40
|
-
const { cfg, to, text, mediaUrl, accountId } = ctx;
|
|
40
|
+
const { cfg, to, text, mediaUrl, accountId, mediaLocalRoots } = ctx;
|
|
41
41
|
const log = typeof ctx.log === "function" ? ctx.log : console.log;
|
|
42
42
|
|
|
43
43
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
@@ -52,7 +52,7 @@ export const palzOutbound = {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (mediaUrl) {
|
|
55
|
-
const ossUrl = await loadMediaAsOssUrl(mediaUrl, log);
|
|
55
|
+
const ossUrl = await loadMediaAsOssUrl(mediaUrl, log, mediaLocalRoots);
|
|
56
56
|
if (ossUrl) {
|
|
57
57
|
contentParts.push({ type: "file", file_url: { url: ossUrl } });
|
|
58
58
|
log(`palz-outbound: [sendMedia] 媒体转换成功: ossUrl=${ossUrl}`);
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface CreatePalzReplyDispatcherParams {
|
|
|
43
43
|
msgId: string;
|
|
44
44
|
msgType?: string;
|
|
45
45
|
groupId?: string;
|
|
46
|
+
mediaLocalRoots?: readonly string[];
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParams) {
|
|
@@ -59,6 +60,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
|
|
|
59
60
|
msgId,
|
|
60
61
|
msgType,
|
|
61
62
|
groupId,
|
|
63
|
+
mediaLocalRoots,
|
|
62
64
|
} = params;
|
|
63
65
|
|
|
64
66
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
@@ -125,7 +127,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
|
|
|
125
127
|
|
|
126
128
|
for (let i = 0; i < mediaUrls.length; i++) {
|
|
127
129
|
log(`${tag}: [DELIVER 媒体] ${i + 1}/${mediaUrls.length} url=${mediaUrls[i].slice(0, 200)}`);
|
|
128
|
-
const ossUrl = await loadMediaAsOssUrl(mediaUrls[i], log);
|
|
130
|
+
const ossUrl = await loadMediaAsOssUrl(mediaUrls[i], log, mediaLocalRoots);
|
|
129
131
|
if (ossUrl) {
|
|
130
132
|
contentParts.push({ type: "file", file_url: { url: ossUrl } });
|
|
131
133
|
log(`${tag}: [DELIVER 媒体转换成功] ${i + 1}/${mediaUrls.length} ossUrl=${ossUrl}`);
|
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