coze_lab 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -105,11 +105,11 @@ else:
105
105
 
106
106
  # --- Configuration ---
107
107
  DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
108
- _COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
108
+ _COZELOOP_CLIENT_ID = "08972682140163281554629748278108.app.coze"
109
109
  _COZE_API = "https://api.coze.cn"
110
110
  _OTEL_SUFFIX = "/v1/loop/opentelemetry"
111
111
  _REFRESH_THRESHOLD = 10 * 60 # refresh when < 10 minutes remain
112
- _DEFAULT_WORKSPACE_ID = "7644910356078837760" # hardcoded spaceID fallback
112
+ _DEFAULT_WORKSPACE_ID = "7649231955045072915" # hardcoded spaceID fallback
113
113
 
114
114
 
115
115
  # --- coze-context parsing -------------------------------------------------
@@ -212,16 +212,41 @@ def _make_finish_event_processor(upload_events: Optional[List[str]] = None):
212
212
  logid = _extract_logid(detail)
213
213
  if logid:
214
214
  print(f"[CozeLoop] 上报失败 logid={logid} (可用 bytedcli log get-logid-log {logid} 排查)", file=sys.stderr)
215
+ hook_log(f"upload FAILED logid={logid} detail={detail[:300]}")
215
216
  else:
216
217
  print(f"[CozeLoop] 上报失败: {detail[:300]}", file=sys.stderr)
218
+ hook_log(f"upload FAILED detail={detail[:300]}")
217
219
  except Exception:
218
220
  pass
219
221
  return _processor
220
222
 
221
223
 
222
224
 
225
+ def _log_file_path() -> str:
226
+ return os.environ.get("COZELOOP_HOOK_LOG", "").strip()
227
+
228
+
229
+ def hook_log(message: str):
230
+ """Append one diagnostic line to the hook log, if configured.
231
+
232
+ onboard writes COZELOOP_HOOK_LOG into settings.local.json (env), so this
233
+ captures runtime upload diagnostics even when stderr is not visible.
234
+ """
235
+ log_path = _log_file_path()
236
+ if not log_path:
237
+ return
238
+ try:
239
+ p = Path(log_path).expanduser()
240
+ p.parent.mkdir(parents=True, exist_ok=True)
241
+ with p.open("a", encoding="utf-8") as f:
242
+ f.write(f"{datetime.now().isoformat()} {message}\n")
243
+ except Exception:
244
+ pass
245
+
246
+
223
247
  def debug_log(message: str):
224
- """Print debug message if debug mode is enabled."""
248
+ """Print debug message if debug mode is enabled; always append to hook log."""
249
+ hook_log(f"DEBUG {message}")
225
250
  if DEBUG:
226
251
  print(f"[COZELOOP_HOOK_DEBUG] {datetime.now().isoformat()} - {message}", file=sys.stderr)
227
252
 
@@ -258,8 +283,6 @@ def _refresh_token(refresh_token: str) -> Optional[str]:
258
283
  data=payload,
259
284
  headers={
260
285
  "Content-Type": "application/json",
261
- "x-tt-env": "ppe_cozelab",
262
- "x-use-ppe": "1",
263
286
  },
264
287
  )
265
288
  with urllib.request.urlopen(req, timeout=10) as resp:
@@ -281,16 +304,22 @@ def _refresh_token(refresh_token: str) -> Optional[str]:
281
304
 
282
305
  def _normalize_api_base_url(url: str) -> str:
283
306
  base = (url or "").strip().rstrip("/")
284
- if base.endswith(_OTEL_SUFFIX + "/v1/traces"):
285
- return base[:-len(_OTEL_SUFFIX + "/v1/traces")].rstrip("/")
286
- if base.endswith("/api/v1/loop/opentelemetry/v1/traces"):
287
- return base[:-len("/v1/loop/opentelemetry/v1/traces")].rstrip("/")
288
- if base.endswith(_OTEL_SUFFIX):
289
- return base[:-len(_OTEL_SUFFIX)].rstrip("/")
290
- if base.endswith("/api/v1/loop/opentelemetry"):
291
- return base[:-len("/v1/loop/opentelemetry")].rstrip("/")
292
- if base.endswith("/api/v1"):
293
- return base[:-len("/v1")].rstrip("/")
307
+ if not base:
308
+ return base
309
+ # 剥除已知 loop 路径后缀,还原纯 API base
310
+ # 关键:含 /api 的变体必须连 /api 整体剥掉,否则残留 /api 会拼出 /api/v1/loop/... → 后端 400。
311
+ # 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免误匹配短后缀。
312
+ suffixes = (
313
+ "/api/v1/loop/opentelemetry/v1/traces",
314
+ _OTEL_SUFFIX + "/v1/traces",
315
+ "/api/v1/loop/opentelemetry",
316
+ _OTEL_SUFFIX,
317
+ "/api/v1",
318
+ "/v1",
319
+ )
320
+ for suffix in suffixes:
321
+ if base.endswith(suffix):
322
+ return base[:-len(suffix)].rstrip("/")
294
323
  return base
295
324
 
296
325
  def get_api_base_url() -> str:
@@ -970,6 +999,11 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
970
999
  api_base_url = get_api_base_url()
971
1000
  if api_base_url:
972
1001
  client_kwargs["api_base_url"] = api_base_url
1002
+ hook_log(
1003
+ f"init client session={session_id} workspace={workspace_id} "
1004
+ f"token={(token[:12] + '...') if token else 'none'} "
1005
+ f"api_base_url={api_base_url or '(default)'}"
1006
+ )
973
1007
  client = cozeloop.new_client(**client_kwargs)
974
1008
 
975
1009
  try:
@@ -42,10 +42,10 @@ from pathlib import Path
42
42
  from typing import Optional, List, Dict, Any
43
43
 
44
44
  # --- Token refresh --------------------------------------------------------
45
- _COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
45
+ _COZELOOP_CLIENT_ID = "08972682140163281554629748278108.app.coze"
46
46
  _COZE_API = "https://api.coze.cn"
47
47
  _REFRESH_THRESHOLD = 10 * 60
48
- _DEFAULT_WORKSPACE_ID = "7644910356078837760" # hardcoded spaceID fallback
48
+ _DEFAULT_WORKSPACE_ID = "7649231955045072915" # hardcoded spaceID fallback
49
49
  _OTEL_SUFFIX = "/v1/loop/opentelemetry"
50
50
 
51
51
 
@@ -195,8 +195,6 @@ def _refresh_token(refresh_tok: str):
195
195
  data=payload,
196
196
  headers={
197
197
  "Content-Type": "application/json",
198
- "x-tt-env": "ppe_cozelab",
199
- "x-use-ppe": "1",
200
198
  },
201
199
  )
202
200
  with urllib.request.urlopen(req, timeout=10) as resp:
@@ -218,16 +216,22 @@ def _refresh_token(refresh_tok: str):
218
216
 
219
217
  def _normalize_api_base_url(url: str) -> str:
220
218
  base = (url or "").strip().rstrip("/")
221
- if base.endswith(_OTEL_SUFFIX + "/v1/traces"):
222
- return base[:-len(_OTEL_SUFFIX + "/v1/traces")].rstrip("/")
223
- if base.endswith("/api/v1/loop/opentelemetry/v1/traces"):
224
- return base[:-len("/v1/loop/opentelemetry/v1/traces")].rstrip("/")
225
- if base.endswith(_OTEL_SUFFIX):
226
- return base[:-len(_OTEL_SUFFIX)].rstrip("/")
227
- if base.endswith("/api/v1/loop/opentelemetry"):
228
- return base[:-len("/v1/loop/opentelemetry")].rstrip("/")
229
- if base.endswith("/api/v1"):
230
- return base[:-len("/v1")].rstrip("/")
219
+ if not base:
220
+ return base
221
+ # 剥除已知 loop 路径后缀,还原纯 API base
222
+ # 关键:含 /api 的变体必须连 /api 整体剥掉,否则残留 /api 会拼出 /api/v1/loop/... → 后端 400。
223
+ # 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免误匹配短后缀。
224
+ suffixes = (
225
+ "/api/v1/loop/opentelemetry/v1/traces",
226
+ _OTEL_SUFFIX + "/v1/traces",
227
+ "/api/v1/loop/opentelemetry",
228
+ _OTEL_SUFFIX,
229
+ "/api/v1",
230
+ "/v1",
231
+ )
232
+ for suffix in suffixes:
233
+ if base.endswith(suffix):
234
+ return base[:-len(suffix)].rstrip("/")
231
235
  return base
232
236
 
233
237
 
@@ -5,16 +5,24 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_INSTANCE_ID } from "@opentelemetry/sema
5
5
  import { hostname } from "os";
6
6
  import { basename, join } from "path";
7
7
  import { createRequire } from "node:module";
8
- import { readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
9
9
  import { homedir } from "os";
10
10
  import http from "http";
11
11
  import https from "https";
12
12
 
13
13
  const require = createRequire(import.meta.url);
14
14
  const { version: PLUGIN_VERSION } = require("../package.json");
15
+ const CLIENT_USER_AGENT = {
16
+ version: PLUGIN_VERSION,
17
+ lang: "nodejs",
18
+ lang_version: process.versions.node,
19
+ os_name: process.platform,
20
+ scene: "cozeloop",
21
+ source: "openapi",
22
+ };
15
23
 
16
24
  // ── Token refresh helpers ─────────────────────────────────────────────────
17
- const _CLIENT_ID = "46371084383473718052118955183420.app.coze";
25
+ const _CLIENT_ID = "08972682140163281554629748278108.app.coze";
18
26
  const _COZE_API = "https://api.coze.cn";
19
27
  const _REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
20
28
  const _CREDS_PATH = join(homedir(), ".cozeloop", "credentials.json");
@@ -36,7 +44,7 @@ async function _refreshToken(refreshTok) {
36
44
  const body = JSON.stringify({ grant_type: "refresh_token", client_id: _CLIENT_ID, refresh_token: refreshTok });
37
45
  const req = https.request(`${_COZE_API}/api/permission/oauth2/token`, {
38
46
  method: "POST",
39
- headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "x-tt-env": "ppe_cozelab", "x-use-ppe": "1" },
47
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
40
48
  }, (res) => {
41
49
  let buf = "";
42
50
  res.on("data", c => buf += c);
@@ -82,24 +90,41 @@ const EXPORT_SUCCESS = 0;
82
90
  const EXPORT_FAILED = 1;
83
91
  const INGEST_TRACE_PATH = "/v1/loop/traces/ingest";
84
92
 
93
+ // 把一行诊断写入 onboard 配置的 logFile(pluginConfig.logFile)。云端 gateway 日志常拿不到
94
+ // (无 DBUS / 无 stdout 落盘),这个文件是排查上报失败的唯一依据。无 logFile 或写失败都静默。
95
+ function fileLog(logFile, message) {
96
+ if (!logFile)
97
+ return;
98
+ try {
99
+ appendFileSync(logFile, `${new Date().toISOString()} ${message}\n`);
100
+ }
101
+ catch {
102
+ /* best-effort, never throw from logging */
103
+ }
104
+ }
105
+
85
106
  function normalizeApiBaseUrl(endpoint) {
86
107
  const base = String(endpoint || "").replace(/\/+$/, "");
87
108
  if (!base)
88
109
  return _COZE_API;
110
+ // 剥除已知 loop 路径后缀,还原纯 API base。
111
+ // 关键:cozeloop ingest 正确端点 <host>/v1/loop/traces/ingest,【不带 /api 前缀】。
112
+ // 含 /api 的变体必须连 /api 整体剥掉 —— 否则残留 /api → /api/v1/loop/traces/ingest → 后端 400。
113
+ // 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免 endsWith 误匹配短后缀。
89
114
  const suffixes = [
90
- "/v1/loop/traces/ingest",
91
115
  "/api/v1/loop/traces/ingest",
92
- "/v1/loop/opentelemetry/v1/traces",
116
+ "/v1/loop/traces/ingest",
93
117
  "/api/v1/loop/opentelemetry/v1/traces",
94
- "/v1/loop/opentelemetry",
118
+ "/v1/loop/opentelemetry/v1/traces",
95
119
  "/api/v1/loop/opentelemetry",
120
+ "/v1/loop/opentelemetry",
96
121
  "/v1/traces",
97
122
  "/api/v1",
123
+ "/v1",
98
124
  ];
99
125
  for (const suffix of suffixes) {
100
126
  if (base.endsWith(suffix)) {
101
- const trimSuffix = suffix.startsWith("/api/") ? suffix.slice("/api".length) : suffix;
102
- return base.slice(0, -trimSuffix.length).replace(/\/+$/, "");
127
+ return base.slice(0, -suffix.length).replace(/\/+$/, "");
103
128
  }
104
129
  }
105
130
  return base;
@@ -195,7 +220,7 @@ function spanToUploadSpan(span, config) {
195
220
  const attrs = span.attributes || {};
196
221
  const { tags, systemTags } = splitAttributes(attrs);
197
222
  const resourceAttrs = span.resource?.attributes || {};
198
- const serviceName = resourceAttrs["service.name"] || config.serviceName || "openclaw-agent";
223
+ const serviceName = resourceAttrs["service.name"] || config.serviceName || "";
199
224
  const spanType = mapSpanType(attrs["cozeloop.span_type"]);
200
225
  const input = attrs["cozeloop.input"] !== undefined ? safeStringify(attrs["cozeloop.input"]) : "";
201
226
  const output = attrs["cozeloop.output"] !== undefined ? safeStringify(attrs["cozeloop.output"]) : "";
@@ -204,7 +229,6 @@ function spanToUploadSpan(span, config) {
204
229
  return {
205
230
  started_at_micros: hrTimeToUnixMicros(span.startTime),
206
231
  log_id: "",
207
- traceId: spanContext.traceId,
208
232
  trace_id: spanContext.traceId,
209
233
  span_id: spanContext.spanId,
210
234
  parent_id: span.parentSpanId || "0",
@@ -260,9 +284,11 @@ class CozeloopIngestExporter {
260
284
  this.url = normalizeIngestUrl(config.endpoint || config.url);
261
285
  this.headers = config.headers || {};
262
286
  this.logger = config.logger;
287
+ this.logFile = config.logFile;
263
288
  this.workspaceId = config.workspaceId;
264
289
  this.serviceName = config.serviceName;
265
290
  this.shutdownRequested = false;
291
+ fileLog(this.logFile, `[ingest] exporter ready url=${this.url} workspaceId=${this.workspaceId}`);
266
292
  }
267
293
  export(spans, resultCallback) {
268
294
  if (!spans || spans.length === 0 || this.shutdownRequested) {
@@ -273,6 +299,7 @@ class CozeloopIngestExporter {
273
299
  .then(() => resultCallback({ code: EXPORT_SUCCESS }))
274
300
  .catch((err) => {
275
301
  this.logger?.error?.(`[CozeloopTrace] CozeLoop ingest export failed: ${err?.message || err}`);
302
+ fileLog(this.logFile, `[ingest] export FAILED url=${this.url} spans=${spans.length} err=${err?.message || err}`);
276
303
  resultCallback({ code: EXPORT_FAILED, error: err });
277
304
  });
278
305
  }
@@ -283,15 +310,18 @@ class CozeloopIngestExporter {
283
310
  serviceName: this.serviceName,
284
311
  })),
285
312
  };
313
+ fileLog(this.logFile, `[ingest] POST url=${this.url} spans=${body.spans.length}`);
286
314
  const res = await postJson(this.url, body, this.headers);
287
315
  if (res.status < 200 || res.status >= 300) {
288
316
  const snippet = String(res.body || "").slice(0, 300);
317
+ fileLog(this.logFile, `[ingest] HTTP ${res.status} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
289
318
  throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
290
319
  }
291
320
  if (res.body) {
292
321
  try {
293
322
  const parsed = JSON.parse(res.body);
294
323
  if (parsed && parsed.code !== undefined && parsed.code !== 0) {
324
+ fileLog(this.logFile, `[ingest] biz-error code=${parsed.code} msg=${parsed.msg || ""} url=${this.url}`);
295
325
  throw new Error(`code ${parsed.code}: ${parsed.msg || res.body.slice(0, 300)}`);
296
326
  }
297
327
  }
@@ -301,6 +331,7 @@ class CozeloopIngestExporter {
301
331
  }
302
332
  }
303
333
  }
334
+ fileLog(this.logFile, `[ingest] OK HTTP ${res.status} spans=${body.spans.length}`);
304
335
  }
305
336
  async forceFlush() {
306
337
  return;
@@ -405,15 +436,17 @@ export class CozeloopExporter {
405
436
  const authorization = this.config.authorization;
406
437
  const workspaceId = this.config.workspaceId;
407
438
  this.api.logger.info(`[CozeloopTrace] Using authorization, workspaceId=${workspaceId}, tokenLength=${authorization?.length}`);
439
+ fileLog(this.config.logFile, `[init] ingestUrl=${normalizeIngestUrl(this.config.endpoint)} workspaceId=${workspaceId} tokenLength=${authorization?.length || 0} plugin=${PLUGIN_VERSION}`);
408
440
  const exporter = new CozeloopIngestExporter({
409
441
  endpoint: this.config.endpoint,
410
442
  logger: this.api.logger,
443
+ logFile: this.config.logFile,
411
444
  workspaceId,
412
445
  serviceName: this.config.serviceName,
413
446
  headers: {
414
447
  "Authorization": authorization,
415
- "x-tt-env": "ppe_cozelab",
416
- "x-use-ppe": "1",
448
+ "User-Agent": `openclaw-cozeloop-trace/${PLUGIN_VERSION} node/${process.versions.node}`,
449
+ "X-Coze-Client-User-Agent": JSON.stringify(CLIENT_USER_AGENT),
417
450
  },
418
451
  });
419
452
  this.provider = new BasicTracerProvider({ resource });
@@ -473,10 +506,11 @@ export class CozeloopExporter {
473
506
  const runtimeTag = {
474
507
  language: "nodejs",
475
508
  library: "openclaw",
509
+ scene: process.env.COZELOOP_SCENE || "custom",
510
+ library_version: null,
511
+ loop_sdk_version: `v${PLUGIN_VERSION}`,
512
+ extra: null,
476
513
  };
477
- if (process.env.COZELOOP_SCENE) {
478
- runtimeTag.scene = process.env.COZELOOP_SCENE;
479
- }
480
514
  const systemTagRuntime = JSON.stringify(runtimeTag);
481
515
  const span = this.tracer.startSpan(spanData.name, {
482
516
  kind: spanKind,
@@ -579,10 +613,11 @@ export class CozeloopExporter {
579
613
  const runtimeTag = {
580
614
  language: "nodejs",
581
615
  library: "openclaw",
616
+ scene: process.env.COZELOOP_SCENE || "custom",
617
+ library_version: null,
618
+ loop_sdk_version: `v${PLUGIN_VERSION}`,
619
+ extra: null,
582
620
  };
583
- if (process.env.COZELOOP_SCENE) {
584
- runtimeTag.scene = process.env.COZELOOP_SCENE;
585
- }
586
621
  const systemTagRuntime = JSON.stringify(runtimeTag);
587
622
  const span = this.tracer.startSpan(spanData.name, {
588
623
  kind: spanKind,
@@ -534,6 +534,7 @@ const cozeloopTracePlugin = {
534
534
  batchInterval: pluginConfig.batchInterval || 5000,
535
535
  enabledHooks: pluginConfig.enabledHooks,
536
536
  disableLocalCredentials: pluginConfig.disableLocalCredentials === true,
537
+ logFile: pluginConfig.logFile,
537
538
  };
538
539
  const exporter = new CozeloopExporter(api, config);
539
540
  // per-agent trace 放行:traceAgentIds 为 onboard 写入的 allowlist(小写归一)。
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-cozeloop-trace",
3
3
  "name": "OpenClaw CozeLoop Trace",
4
- "version": "0.1.16",
4
+ "version": "0.1.18",
5
5
  "description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
6
6
  "type": "plugin",
7
7
  "entry": "./dist/index.js",
@@ -54,6 +54,11 @@
54
54
  "type": "boolean",
55
55
  "default": false,
56
56
  "description": "Disable ~/.cozeloop credentials refresh and use configured authorization only"
57
+ },
58
+ "logFile": {
59
+ "type": "string",
60
+ "default": "",
61
+ "description": "Append trace ingest diagnostics (URL, span count, HTTP status, failure body) to this file"
57
62
  }
58
63
  }
59
64
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cozeloop/openclaw-cozeloop-trace",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "OpenClaw Plugin for reporting traces to CozeLoop via OpenTelemetry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -7,7 +7,7 @@ Writes the fresh token back so subsequent Stop hooks pick it up.
7
7
  import json, os, sys, time, urllib.request
8
8
  from pathlib import Path
9
9
 
10
- CLIENT_ID = "46371084383473718052118955183420.app.coze"
10
+ CLIENT_ID = "08972682140163281554629748278108.app.coze"
11
11
  COZE_API = "https://api.coze.cn"
12
12
  THRESHOLD = 10 * 60 # 10 minutes
13
13
  CREDS = Path.home() / ".cozeloop" / "credentials.json"
@@ -27,8 +27,6 @@ def refresh(rt):
27
27
  req = urllib.request.Request(f"{COZE_API}/api/permission/oauth2/token",
28
28
  data=body, headers={
29
29
  "Content-Type":"application/json",
30
- "x-tt-env":"ppe_cozelab",
31
- "x-use-ppe":"1",
32
30
  })
33
31
  with urllib.request.urlopen(req, timeout=10) as r:
34
32
  d = json.loads(r.read())