coze_lab 0.1.51 → 0.1.52

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.js CHANGED
@@ -20,12 +20,43 @@ class CloudAbort extends Error {}
20
20
  // 兼容两种形态:JSON 里的 "logid":"xxx" / detail.logid,以及裸 logid=xxx。
21
21
  function extractLogid(text) {
22
22
  if (!text) return '';
23
- let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9]+)"/);
23
+ let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
24
24
  if (m) return m[1];
25
- m = text.match(/logid[=:\s]+([a-zA-Z0-9]+)/i);
25
+ m = text.match(/"log_id"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
26
+ if (m) return m[1];
27
+ m = text.match(/"Logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
28
+ if (m) return m[1];
29
+ m = text.match(/logid[=:\s]+([a-zA-Z0-9._:-]+)/i);
26
30
  return m ? m[1] : '';
27
31
  }
28
32
 
33
+ function headerString(value) {
34
+ if (Array.isArray(value)) return value.find(Boolean) || '';
35
+ return value ? String(value) : '';
36
+ }
37
+
38
+ function extractLogidFromHeaders(headers) {
39
+ if (!headers) return '';
40
+ const candidates = [
41
+ 'x-tt-logid',
42
+ 'x-tt-log-id',
43
+ 'x-log-id',
44
+ 'x-logid',
45
+ 'x-request-id',
46
+ 'x-tt-trace-id',
47
+ 'trace-id',
48
+ ];
49
+ for (const key of candidates) {
50
+ const value = headerString(headers[key] || headers[key.toLowerCase()] || headers[key.toUpperCase()]);
51
+ if (value) return value;
52
+ }
53
+ return '';
54
+ }
55
+
56
+ function formatLogid(logid) {
57
+ return logid ? `, logid=${logid}` : ', logid=';
58
+ }
59
+
29
60
  // 输出结构化结果行(仅云端模式)。
30
61
  function emitCloudResult() {
31
62
  if (!CLOUD_MODE) return;
@@ -1108,7 +1139,7 @@ function httpsPost(url, body, extraHeaders) {
1108
1139
  (res) => {
1109
1140
  let buf = '';
1110
1141
  res.on('data', c => buf += c);
1111
- res.on('end', () => resolve({ status: res.statusCode, body: buf }));
1142
+ res.on('end', () => resolve({ status: res.statusCode, body: buf, headers: res.headers || {}, logid: extractLogidFromHeaders(res.headers) || extractLogid(buf) }));
1112
1143
  }
1113
1144
  );
1114
1145
  req.on('error', reject);
@@ -1166,7 +1197,7 @@ def extract_logid(text):
1166
1197
  if idx >= 0:
1167
1198
  out = []
1168
1199
  for ch in text[idx + len(marker):]:
1169
- if ch.isalnum():
1200
+ if ch.isalnum() or ch in "._:-":
1170
1201
  out.append(ch)
1171
1202
  else:
1172
1203
  break
@@ -1255,7 +1286,7 @@ except Exception as e:
1255
1286
  const body = parsed?.body || result.stderr || result.stdout || (result.timedOut ? 'SDK selfcheck timed out' : '');
1256
1287
  const success = result.code === 0 && parsed?.success === true;
1257
1288
  if (success) {
1258
- ok(`trace 上报成功 (pair_code=${pair})`);
1289
+ ok(`trace 上报成功 (pair_code=${pair}${formatLogid(parsed?.logid || '')})`);
1259
1290
  info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
1260
1291
  } else {
1261
1292
  warn(`trace 上报失败: SDK selfcheck exit ${result.code}`);
@@ -1263,7 +1294,7 @@ except Exception as e:
1263
1294
  const snippet = String(body || '').slice(0, 300);
1264
1295
  if (snippet) console.log(snippet);
1265
1296
  }
1266
- return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || '' };
1297
+ return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || extractLogid(body) || '' };
1267
1298
  }
1268
1299
 
1269
1300
  async function verifyCloudTraceReport(token, workspaceId, pairCode, tokenSource) {
@@ -1330,14 +1361,14 @@ async function verifyCloudTraceReport(token, workspaceId, pairCode, tokenSource)
1330
1361
  );
1331
1362
  const success = res.status >= 200 && res.status < 300;
1332
1363
  if (success) {
1333
- ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
1364
+ ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair}${formatLogid(res.logid || '')})`);
1334
1365
  info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
1335
1366
  } else {
1336
- warn(`trace 上报失败: HTTP ${res.status}`);
1367
+ warn(`trace 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')}`);
1337
1368
  const snippet = (res.body || '').slice(0, 300);
1338
1369
  if (snippet) console.log(snippet);
1339
1370
  }
1340
- return { success, status: res.status, body: res.body || '', traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: extractLogid(res.body || '') };
1371
+ return { success, status: res.status, body: res.body || '', traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: res.logid || extractLogid(res.body || '') };
1341
1372
  } catch (e) {
1342
1373
  warn(`trace 上报失败: ${e.message}`);
1343
1374
  return { success: false, status: 0, body: e.message, traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: extractLogid(e.message) };
@@ -1396,14 +1427,14 @@ async function verifyTraceReport(token, workspaceId, pairCode, tracesUrl) {
1396
1427
 
1397
1428
  const success = res.status >= 200 && res.status < 300;
1398
1429
  if (success) {
1399
- ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
1430
+ ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair}${formatLogid(res.logid || '')})`);
1400
1431
  info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
1401
1432
  } else {
1402
- warn(`trace 上报失败: HTTP ${res.status}`);
1433
+ warn(`trace 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')}`);
1403
1434
  const snippet = (res.body || '').slice(0, 300);
1404
1435
  if (snippet) console.log(snippet);
1405
1436
  }
1406
- return { success, status: res.status, body: res.body, traceId, pairCode: pair };
1437
+ return { success, status: res.status, body: res.body, traceId, pairCode: pair, logid: res.logid || extractLogid(res.body || '') };
1407
1438
  }
1408
1439
 
1409
1440
  // ── OpenClaw 专属上报链路校验 ──────────────────────────────────────────────
@@ -1500,9 +1531,9 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
1500
1531
  const tokenPrefix = authHeader.replace(/^Bearer\s+/i, '').slice(0, 12);
1501
1532
 
1502
1533
  if (success) {
1503
- ok(`openclaw 插件实际 token 上报正常 (token=${tokenPrefix}..., HTTP ${res.status})`);
1534
+ ok(`openclaw 插件实际 token 上报正常 (token=${tokenPrefix}..., HTTP ${res.status}${formatLogid(res.logid || '')})`);
1504
1535
  } else {
1505
- warn(`openclaw 插件实际 token 上报失败: HTTP ${res.status} (token=${tokenPrefix}...)`);
1536
+ warn(`openclaw 插件实际 token 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')} (token=${tokenPrefix}...)`);
1506
1537
  const snippet = (res.body || '').slice(0, 300);
1507
1538
  if (snippet) console.log(snippet);
1508
1539
  // 4100/401 = 该 token 已失效。指出根因与修复方式。
@@ -1512,7 +1543,7 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
1512
1543
  }
1513
1544
  }
1514
1545
 
1515
- return { success, status: res.status, body: res.body || '' };
1546
+ return { success, status: res.status, body: res.body || '', logid: res.logid || extractLogid(res.body || '') };
1516
1547
  }
1517
1548
 
1518
1549
  function httpsGet(url, headers) {
@@ -1745,6 +1776,7 @@ async function main() {
1745
1776
  : await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
1746
1777
  if (verifyResult.success) {
1747
1778
  cloudResult.verify = 'ok';
1779
+ cloudResult.logid = verifyResult.logid || extractLogid(verifyResult.body) || cloudResult.logid;
1748
1780
  } else if (CLOUD_MODE) {
1749
1781
  // 云端:注入已成功,验证失败不阻断(放行),记录结果供后台弹 warning。
1750
1782
  cloudResult.verify = 'fail';
@@ -1760,6 +1792,7 @@ async function main() {
1760
1792
  'ERROR: trace 上报自检失败',
1761
1793
  '',
1762
1794
  `HTTP ${verifyResult.status}`,
1795
+ `logid=${verifyResult.logid || extractLogid(verifyResult.body) || ''}`,
1763
1796
  (verifyResult.body || '').slice(0, 300),
1764
1797
  '',
1765
1798
  'hook 配置已写入,但上报链路未打通。',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -47,6 +47,48 @@ function fileLog(logFile, message) {
47
47
  /* best-effort, never throw from logging */
48
48
  }
49
49
  }
50
+ function headerString(value) {
51
+ if (Array.isArray(value))
52
+ return value.find(Boolean) || "";
53
+ return value ? String(value) : "";
54
+ }
55
+ function extractLogidFromHeaders(headers) {
56
+ if (!headers)
57
+ return "";
58
+ const candidates = [
59
+ "x-tt-logid",
60
+ "x-tt-log-id",
61
+ "x-log-id",
62
+ "x-logid",
63
+ "x-request-id",
64
+ "x-tt-trace-id",
65
+ "trace-id",
66
+ ];
67
+ for (const key of candidates) {
68
+ const value = headerString(headers[key] || headers[key.toLowerCase()] || headers[key.toUpperCase()]);
69
+ if (value)
70
+ return value;
71
+ }
72
+ return "";
73
+ }
74
+ function extractLogid(text) {
75
+ if (!text)
76
+ return "";
77
+ let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
78
+ if (m)
79
+ return m[1];
80
+ m = text.match(/"log_id"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
81
+ if (m)
82
+ return m[1];
83
+ m = text.match(/"Logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
84
+ if (m)
85
+ return m[1];
86
+ m = text.match(/logid[=:\s]+([a-zA-Z0-9._:-]+)/i);
87
+ return m ? m[1] : "";
88
+ }
89
+ function formatLogid(logid) {
90
+ return logid ? ` logid=${logid}` : " logid=";
91
+ }
50
92
 
51
93
  function normalizeApiBaseUrl(endpoint) {
52
94
  const base = String(endpoint || "").replace(/\/+$/, "");
@@ -215,7 +257,12 @@ function postJson(url, body, headers) {
215
257
  }, (res) => {
216
258
  let buf = "";
217
259
  res.on("data", c => buf += c);
218
- res.on("end", () => resolve({ status: res.statusCode || 0, body: buf }));
260
+ res.on("end", () => resolve({
261
+ status: res.statusCode || 0,
262
+ body: buf,
263
+ headers: res.headers || {},
264
+ logid: extractLogidFromHeaders(res.headers) || extractLogid(buf),
265
+ }));
219
266
  });
220
267
  req.on("error", reject);
221
268
  req.setTimeout(15000, () => req.destroy(new Error("CozeLoop ingest export timed out")));
@@ -305,8 +352,8 @@ class CozeloopIngestExporter {
305
352
  const res = await postJson(this.url, body, this.headers);
306
353
  if (res.status < 200 || res.status >= 300) {
307
354
  const snippet = String(res.body || "").slice(0, 300);
308
- fileLog(this.logFile, `[ingest] HTTP ${res.status} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
309
- throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
355
+ fileLog(this.logFile, `[ingest] HTTP ${res.status}${formatLogid(res.logid)} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
356
+ throw new Error(`HTTP ${res.status}${formatLogid(res.logid)}${snippet ? `: ${snippet}` : ""}`);
310
357
  }
311
358
  if (res.body) {
312
359
  try {
@@ -322,7 +369,7 @@ class CozeloopIngestExporter {
322
369
  }
323
370
  }
324
371
  }
325
- fileLog(this.logFile, `[ingest] OK HTTP ${res.status} spans=${body.spans.length}${retry ? " retry=1" : ""}`);
372
+ fileLog(this.logFile, `[ingest] OK HTTP ${res.status}${formatLogid(res.logid)} spans=${body.spans.length}${retry ? " retry=1" : ""}`);
326
373
  }
327
374
  async forceFlush() {
328
375
  // Wait for every in-flight POST to settle. allSettled (not all) so one
@@ -196,6 +196,54 @@ function formatAssistantOutput(content, stopReason) {
196
196
  ],
197
197
  };
198
198
  }
199
+ function usageInt(usage, keys) {
200
+ if (!usage || typeof usage !== "object")
201
+ return 0;
202
+ for (const key of keys) {
203
+ const value = usage[key];
204
+ if (value === undefined || value === null || value === "")
205
+ continue;
206
+ const n = typeof value === "number" ? value : Number(value);
207
+ if (Number.isFinite(n) && n > 0)
208
+ return Math.trunc(n);
209
+ }
210
+ return 0;
211
+ }
212
+ function readTokenUsage(...usages) {
213
+ const inputKeys = [
214
+ "input",
215
+ "input_tokens",
216
+ "inputTokens",
217
+ "prompt_tokens",
218
+ "promptTokens",
219
+ "prompt",
220
+ ];
221
+ const outputKeys = [
222
+ "output",
223
+ "output_tokens",
224
+ "outputTokens",
225
+ "completion_tokens",
226
+ "completionTokens",
227
+ "completion",
228
+ ];
229
+ const totalKeys = ["total", "total_tokens", "totalTokens"];
230
+ let input = 0;
231
+ let output = 0;
232
+ let total = 0;
233
+ for (const usage of usages) {
234
+ if (!usage || typeof usage !== "object")
235
+ continue;
236
+ if (input === 0)
237
+ input = usageInt(usage, inputKeys);
238
+ if (output === 0)
239
+ output = usageInt(usage, outputKeys);
240
+ if (total === 0)
241
+ total = usageInt(usage, totalKeys);
242
+ if (input > 0 && output > 0 && total > 0)
243
+ break;
244
+ }
245
+ return { input, output, total };
246
+ }
199
247
  function convertAssistantContentForMessages(content) {
200
248
  if (!Array.isArray(content))
201
249
  return [{ type: "text", text: String(content ?? "") }];
@@ -901,11 +949,15 @@ const cozeloopTracePlugin = {
901
949
  const model = entry.model || ctx.lastModelId || "unknown";
902
950
  const spanStartTime = prevWrittenAt;
903
951
  const spanEndTime = entryWrittenAt;
952
+ const tokenUsage = readTokenUsage(entry.usage);
904
953
  const modelSpan = createSpan(ctx, channelId, `${provider}/${model}`, "model", spanStartTime, spanEndTime, {
905
954
  "gen_ai.provider.name": provider,
906
955
  "gen_ai.request.model": model,
907
- "gen_ai.usage.input_tokens": entry.usage?.input ?? 0,
908
- "gen_ai.usage.output_tokens": entry.usage?.output ?? 0,
956
+ "gen_ai.usage.input_tokens": tokenUsage.input,
957
+ "gen_ai.usage.output_tokens": tokenUsage.output,
958
+ "gen_ai.usage.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
959
+ "input_tokens": tokenUsage.input,
960
+ "output_tokens": tokenUsage.output,
909
961
  "react_round": reactRound,
910
962
  }, { messages: reactMessages.map((msg) => safeClone(msg)) }, formatAssistantOutput(entry.content, entry.stopReason));
911
963
  await exporter.export(modelSpan);
@@ -1269,13 +1321,15 @@ const cozeloopTracePlugin = {
1269
1321
  }
1270
1322
  }
1271
1323
  const lastAssistantUsage = event.lastAssistant?.usage;
1272
- const inputTokens = event.usage?.input ?? lastAssistantUsage?.input ?? 0;
1273
- const outputTokens = event.usage?.output ?? lastAssistantUsage?.output ?? 0;
1324
+ const tokenUsage = readTokenUsage(event.usage, lastAssistantUsage);
1274
1325
  const spanAttributes = {
1275
1326
  "gen_ai.provider.name": event.provider,
1276
1327
  "gen_ai.request.model": event.model,
1277
- "gen_ai.usage.input_tokens": inputTokens,
1278
- "gen_ai.usage.output_tokens": outputTokens,
1328
+ "gen_ai.usage.input_tokens": tokenUsage.input,
1329
+ "gen_ai.usage.output_tokens": tokenUsage.output,
1330
+ "gen_ai.usage.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
1331
+ "input_tokens": tokenUsage.input,
1332
+ "output_tokens": tokenUsage.output,
1279
1333
  };
1280
1334
  const finalOutput = formatAssistantOutput(event.assistantTexts?.map((t) => ({ type: "text", text: t })) ?? [], "stop");
1281
1335
  const span = createSpan(ctx, channelId, `${event.provider}/${event.model}`, "model", startTime, now, spanAttributes, llmInput, finalOutput);
@@ -1595,11 +1649,12 @@ const cozeloopTracePlugin = {
1595
1649
  const active = resolveActiveAgentContext(hookCtx, rawChannelId);
1596
1650
  const ctx = active?.ctx || getOrCreateContext(rawChannelId, undefined, "agent_end").ctx;
1597
1651
  const channelId = active?.channelId || rawChannelId;
1652
+ const tokenUsage = readTokenUsage(event.usage);
1598
1653
  finalizeTrace(ctx, channelId, {
1599
1654
  "agent.duration_ms": event.durationMs || 0,
1600
1655
  "agent.message_count": event.messageCount || 0,
1601
1656
  "agent.tool_call_count": event.toolCallCount || 0,
1602
- "agent.total_tokens": event.usage?.total || 0,
1657
+ "agent.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
1603
1658
  }, { usage: event.usage, cost: event.cost });
1604
1659
  });
1605
1660
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-cozeloop-trace",
3
3
  "name": "OpenClaw CozeLoop Trace",
4
- "version": "0.1.20",
4
+ "version": "0.1.21",
5
5
  "description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
6
6
  "type": "plugin",
7
7
  "entry": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cozeloop/openclaw-cozeloop-trace",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "OpenClaw Plugin for reporting traces to CozeLoop via OpenTelemetry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",