coze_lab 0.1.27 → 0.1.28

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.27",
3
+ "version": "0.1.28",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -14,6 +14,9 @@
14
14
  "bin": {
15
15
  "coze_lab": "index.js"
16
16
  },
17
+ "scripts": {
18
+ "test": "node --test test/*.test.js"
19
+ },
17
20
  "files": [
18
21
  "index.js",
19
22
  "scripts/claude-code/cozeloop_hook.py",
@@ -195,7 +195,7 @@ def _extract_logid(msg: str) -> str:
195
195
  return "".join(logid)
196
196
 
197
197
 
198
- def _make_finish_event_processor():
198
+ def _make_finish_event_processor(upload_events: Optional[List[str]] = None):
199
199
  """Return a trace_finish_event_processor that surfaces failures + logid.
200
200
 
201
201
  The CozeLoop SDK calls this for each flush event; on failure we print the
@@ -207,6 +207,8 @@ def _make_finish_event_processor():
207
207
  if not getattr(info, "is_event_fail", False):
208
208
  return
209
209
  detail = getattr(info, "detail_msg", "") or ""
210
+ if upload_events is not None:
211
+ upload_events.append(detail or "trace export failed")
210
212
  logid = _extract_logid(detail)
211
213
  if logid:
212
214
  print(f"[CozeLoop] 上报失败 logid={logid} (可用 bytedcli log get-logid-log {logid} 排查)", file=sys.stderr)
@@ -254,7 +256,11 @@ def _refresh_token(refresh_token: str) -> Optional[str]:
254
256
  req = urllib.request.Request(
255
257
  f"{_COZE_API}/api/permission/oauth2/token",
256
258
  data=payload,
257
- headers={"Content-Type": "application/json"},
259
+ headers={
260
+ "Content-Type": "application/json",
261
+ "x-tt-env": "ppe_cozelab",
262
+ "x-use-ppe": "1",
263
+ },
258
264
  )
259
265
  with urllib.request.urlopen(req, timeout=10) as resp:
260
266
  data = json.loads(resp.read())
@@ -292,28 +298,48 @@ def get_api_base_url() -> str:
292
298
  os.environ.get("COZELOOP_API_BASE_URL", "")
293
299
  )
294
300
 
301
+ def _token_from_credentials() -> Optional[str]:
302
+ creds = _load_credentials()
303
+ if not creds:
304
+ return None
305
+ expires_at_sec = creds.get("expires_at", 0) / 1000
306
+ remaining = expires_at_sec - time.time()
307
+ if remaining > _REFRESH_THRESHOLD:
308
+ debug_log(f"Cached token valid, expires in {int(remaining)}s.")
309
+ return creds.get("access_token")
310
+ if creds.get("refresh_token"):
311
+ debug_log(f"Token expiring in {int(remaining)}s, refreshing...")
312
+ new_token = _refresh_token(creds["refresh_token"])
313
+ if new_token:
314
+ return new_token
315
+ debug_log("Refresh failed.")
316
+ return None
317
+
318
+
295
319
  def get_fresh_token() -> Optional[str]:
296
- """Return a valid access token, refreshing if needed. Falls back to env var."""
320
+ """Return a valid access token, refreshing if needed."""
297
321
  env_token = os.environ.get("COZELOOP_API_TOKEN")
298
322
  env_coze_token = os.environ.get("COZE_API_TOKEN")
323
+ is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
324
+ if is_cloud:
325
+ return env_token or env_coze_token or _token_from_credentials()
326
+
327
+ # Local onboard used to write a short-lived access token into settings.local.json.
328
+ # If credentials exist but cannot be refreshed, do not fall back to that stale
329
+ # env token; failing closed keeps state from advancing on a known-bad token.
330
+ creds = _load_credentials()
331
+ if creds:
332
+ return _token_from_credentials()
333
+ if env_token and not env_coze_token:
334
+ token = _token_from_credentials()
335
+ if token:
336
+ return token
337
+ return env_token
299
338
  if env_token:
300
339
  return env_token
301
340
  if env_coze_token:
302
341
  return env_coze_token
303
- creds = _load_credentials()
304
- if creds:
305
- expires_at_sec = creds.get("expires_at", 0) / 1000
306
- remaining = expires_at_sec - time.time()
307
- if remaining > _REFRESH_THRESHOLD:
308
- debug_log(f"Cached token valid, expires in {int(remaining)}s.")
309
- return creds["access_token"]
310
- if creds.get("refresh_token"):
311
- debug_log(f"Token expiring in {int(remaining)}s, refreshing...")
312
- new_token = _refresh_token(creds["refresh_token"])
313
- if new_token:
314
- return new_token
315
- debug_log("Refresh failed, falling back to env var.")
316
- return None
342
+ return _token_from_credentials()
317
343
 
318
344
  # -------------------------------------------------------------------------
319
345
 
@@ -931,10 +957,11 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
931
957
  creds = _load_credentials()
932
958
  workspace_id = (creds or {}).get("workspace_id") or os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
933
959
  os.environ["COZELOOP_WORKSPACE_ID"] = workspace_id
960
+ upload_events: List[str] = []
934
961
  client_kwargs = {
935
962
  "ultra_large_report": True,
936
963
  "upload_timeout": 120,
937
- "trace_finish_event_processor": _make_finish_event_processor(),
964
+ "trace_finish_event_processor": _make_finish_event_processor(upload_events),
938
965
  }
939
966
  if workspace_id:
940
967
  client_kwargs["workspace_id"] = workspace_id
@@ -1391,11 +1418,18 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1391
1418
  except Exception as e:
1392
1419
  print(f"[CozeLoop] 上报失败 ✗ {e}", file=sys.stderr)
1393
1420
  debug_log(f"An error occurred while sending traces to CozeLoop: {e}")
1421
+ return None
1394
1422
  finally:
1395
1423
  # Crucial: close the client to ensure all buffered traces are sent.
1396
1424
  client.close()
1397
1425
  debug_log("CozeLoop client closed.")
1398
1426
 
1427
+ if upload_events:
1428
+ debug_log(f"Upload failed, state not advanced. Last failure: {upload_events[-1][:500]}")
1429
+ return None
1430
+
1431
+ return True
1432
+
1399
1433
 
1400
1434
  # --- Hook Input ---
1401
1435
 
@@ -1505,7 +1539,10 @@ def main():
1505
1539
  debug_log("No coze-context found in any turn (incl. history), skipping upload.")
1506
1540
  return
1507
1541
  print(f"[CozeLoop] 开始上报: session={session_id}, turns={len(turns)}", file=sys.stderr)
1508
- send_turns_to_cozeloop(turns, session_id, history_turns)
1542
+ uploaded = send_turns_to_cozeloop(turns, session_id, history_turns)
1543
+ if uploaded is None:
1544
+ debug_log("Send failed, state not advanced.")
1545
+ return
1509
1546
 
1510
1547
  # Update state with the new last processed line number
1511
1548
  last_line_in_batch = max(msg.get("_line_number", 0) for msg in new_messages)
@@ -137,7 +137,7 @@ def _extract_logid(msg: str) -> str:
137
137
  return "".join(logid)
138
138
 
139
139
 
140
- def _make_finish_event_processor():
140
+ def _make_finish_event_processor(upload_events: Optional[List[str]] = None):
141
141
  """Return a trace_finish_event_processor that surfaces failures + logid.
142
142
 
143
143
  The CozeLoop SDK calls this for each flush event; on failure we print the
@@ -150,6 +150,8 @@ def _make_finish_event_processor():
150
150
  hook_log("upload success")
151
151
  return
152
152
  detail = getattr(info, "detail_msg", "") or ""
153
+ if upload_events is not None:
154
+ upload_events.append(detail or "trace export failed")
153
155
  logid = _extract_logid(detail)
154
156
  if logid:
155
157
  hook_log(f"upload failed logid={logid} detail={detail[:500]}")
@@ -191,15 +193,21 @@ def _refresh_token(refresh_tok: str):
191
193
  req = urllib.request.Request(
192
194
  f"{_COZE_API}/api/permission/oauth2/token",
193
195
  data=payload,
194
- headers={"Content-Type": "application/json"},
196
+ headers={
197
+ "Content-Type": "application/json",
198
+ "x-tt-env": "ppe_cozelab",
199
+ "x-use-ppe": "1",
200
+ },
195
201
  )
196
202
  with urllib.request.urlopen(req, timeout=10) as resp:
197
203
  data = json.loads(resp.read())
198
204
  if data.get("access_token"):
205
+ existing = _load_credentials() or {}
199
206
  creds = {
200
207
  "access_token": data["access_token"],
201
208
  "refresh_token": data.get("refresh_token", refresh_tok),
202
- "expires_at": data.get("expires_in", 0) * 1000 # unix timestamp in seconds
209
+ "expires_at": data.get("expires_in", 0) * 1000, # unix timestamp in seconds
210
+ "workspace_id": existing.get("workspace_id", ""),
203
211
  }
204
212
  _save_credentials(creds)
205
213
  return creds["access_token"]
@@ -229,23 +237,43 @@ def get_api_base_url() -> str:
229
237
  )
230
238
 
231
239
 
240
+ def _token_from_credentials():
241
+ creds = _load_credentials()
242
+ if not creds:
243
+ return None
244
+ remaining = creds.get("expires_at", 0) / 1000 - time.time()
245
+ if remaining > _REFRESH_THRESHOLD:
246
+ return creds.get("access_token")
247
+ if creds.get("refresh_token"):
248
+ new_token = _refresh_token(creds["refresh_token"])
249
+ if new_token:
250
+ return new_token
251
+ return None
252
+
253
+
232
254
  def get_fresh_token():
233
255
  env_token = os.environ.get("COZELOOP_API_TOKEN")
234
256
  env_coze_token = os.environ.get("COZE_API_TOKEN")
257
+ is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
258
+ if is_cloud:
259
+ return env_token or env_coze_token or _token_from_credentials()
260
+
261
+ # Local onboard used to write a short-lived access token into cozeloop.env.
262
+ # If credentials exist but cannot be refreshed, do not fall back to that stale
263
+ # env token; failing closed keeps state from advancing on a known-bad token.
264
+ creds = _load_credentials()
265
+ if creds:
266
+ return _token_from_credentials()
267
+ if env_token and not env_coze_token:
268
+ token = _token_from_credentials()
269
+ if token:
270
+ return token
271
+ return env_token
235
272
  if env_token:
236
273
  return env_token
237
274
  if env_coze_token:
238
275
  return env_coze_token
239
- creds = _load_credentials()
240
- if creds:
241
- remaining = creds.get("expires_at", 0) / 1000 - time.time()
242
- if remaining > _REFRESH_THRESHOLD:
243
- return creds["access_token"]
244
- if creds.get("refresh_token"):
245
- new_token = _refresh_token(creds["refresh_token"])
246
- if new_token:
247
- return new_token
248
- return None
276
+ return _token_from_credentials()
249
277
  # -------------------------------------------------------------------------
250
278
 
251
279
  # --- SDK Import ---
@@ -966,10 +994,11 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
966
994
  workspace_id = (creds or {}).get("workspace_id") or os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
967
995
  os.environ["COZELOOP_WORKSPACE_ID"] = workspace_id
968
996
  hook_log(f"workspace_id={workspace_id}")
997
+ upload_events: List[str] = []
969
998
  client_kwargs = {
970
999
  "ultra_large_report": True,
971
1000
  "upload_timeout": 120,
972
- "trace_finish_event_processor": _make_finish_event_processor(),
1001
+ "trace_finish_event_processor": _make_finish_event_processor(upload_events),
973
1002
  }
974
1003
  api_base_url = get_api_base_url()
975
1004
  if api_base_url:
@@ -1352,6 +1381,10 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
1352
1381
  hook_log("client closed")
1353
1382
  debug_log("CozeLoop client closed.")
1354
1383
 
1384
+ if upload_events:
1385
+ hook_log(f"upload failed state not advanced failures={len(upload_events)} detail={upload_events[-1][:500]}")
1386
+ return None
1387
+
1355
1388
  return ctx
1356
1389
 
1357
1390
 
@@ -44,10 +44,12 @@ async function _refreshToken(refreshTok) {
44
44
  try {
45
45
  const d = JSON.parse(buf);
46
46
  if (d.access_token) {
47
+ const existing = _loadCreds() ?? {};
47
48
  const creds = {
48
49
  access_token: d.access_token,
49
50
  refresh_token: d.refresh_token ?? refreshTok,
50
51
  expires_at: (d.expires_in ?? 0) * 1000, // unix timestamp in seconds
52
+ workspace_id: existing.workspace_id ?? "",
51
53
  };
52
54
  _saveCreds(creds);
53
55
  resolve(creds.access_token);
@@ -62,7 +64,8 @@ async function _refreshToken(refreshTok) {
62
64
  });
63
65
  }
64
66
 
65
- async function getRefreshedToken(currentAuthorization) {
67
+ async function getRefreshedToken(currentAuthorization, opts = {}) {
68
+ if (opts.disableLocalCredentials) return currentAuthorization;
66
69
  const creds = _loadCreds();
67
70
  if (!creds) return currentAuthorization; // no creds file, keep as-is
68
71
  const remaining = (creds.expires_at ?? 0) - Date.now();
@@ -71,7 +74,7 @@ async function getRefreshedToken(currentAuthorization) {
71
74
  const newToken = await _refreshToken(creds.refresh_token);
72
75
  if (newToken) return `Bearer ${newToken}`;
73
76
  }
74
- return currentAuthorization; // fallback
77
+ return null;
75
78
  }
76
79
  // ─────────────────────────────────────────────────────────────────────────
77
80
 
@@ -131,7 +134,9 @@ export class CozeloopExporter {
131
134
  return attrs;
132
135
  }
133
136
  async refreshAuthIfNeeded() {
134
- const fresh = await getRefreshedToken(this.config.authorization);
137
+ const fresh = await getRefreshedToken(this.config.authorization, {
138
+ disableLocalCredentials: this.config.disableLocalCredentials,
139
+ });
135
140
  if (fresh && fresh !== this.config.authorization) {
136
141
  this.api.logger.info("[CozeloopTrace] Token refreshed, re-initializing exporter...");
137
142
  this.config.authorization = fresh;
@@ -143,6 +148,9 @@ export class CozeloopExporter {
143
148
  this.provider = null;
144
149
  this.tracer = null;
145
150
  }
151
+ } else if (fresh === null) {
152
+ this.api.logger.error("[CozeloopTrace] Local credentials exist but token refresh failed; refusing to reuse stale authorization.");
153
+ this.config.authorization = "";
146
154
  }
147
155
  }
148
156
  async ensureInitialized() {
@@ -513,6 +513,7 @@ const cozeloopTracePlugin = {
513
513
  batchSize: pluginConfig.batchSize || 10,
514
514
  batchInterval: pluginConfig.batchInterval || 5000,
515
515
  enabledHooks: pluginConfig.enabledHooks,
516
+ disableLocalCredentials: pluginConfig.disableLocalCredentials === true,
516
517
  };
517
518
  const exporter = new CozeloopExporter(api, config);
518
519
  // 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.12",
4
+ "version": "0.1.13",
5
5
  "description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
6
6
  "type": "plugin",
7
7
  "entry": "./dist/index.js",
@@ -49,6 +49,11 @@
49
49
  "type": "string"
50
50
  },
51
51
  "description": "List of hooks to enable (if not set, all hooks are enabled)"
52
+ },
53
+ "disableLocalCredentials": {
54
+ "type": "boolean",
55
+ "default": false,
56
+ "description": "Disable ~/.cozeloop credentials refresh and use configured authorization only"
52
57
  }
53
58
  }
54
59
  }
@@ -25,13 +25,19 @@ def refresh(rt):
25
25
  try:
26
26
  body = json.dumps({"grant_type":"refresh_token","client_id":CLIENT_ID,"refresh_token":rt}).encode()
27
27
  req = urllib.request.Request(f"{COZE_API}/api/permission/oauth2/token",
28
- data=body, headers={"Content-Type":"application/json"})
28
+ data=body, headers={
29
+ "Content-Type":"application/json",
30
+ "x-tt-env":"ppe_cozelab",
31
+ "x-use-ppe":"1",
32
+ })
29
33
  with urllib.request.urlopen(req, timeout=10) as r:
30
34
  d = json.loads(r.read())
31
35
  if d.get("access_token"):
36
+ existing = load() or {}
32
37
  save({"access_token":d["access_token"],
33
38
  "refresh_token":d.get("refresh_token",rt),
34
- "expires_at":d.get("expires_in",0)*1000})
39
+ "expires_at":d.get("expires_in",0)*1000,
40
+ "workspace_id":existing.get("workspace_id","")})
35
41
  return True
36
42
  except Exception as e:
37
43
  print(f"[cozeloop_refresh] refresh failed: {e}", file=sys.stderr)
@@ -50,4 +56,3 @@ def main():
50
56
  print("[cozeloop_refresh] refresh failed, token may expire soon", file=sys.stderr)
51
57
 
52
58
  main()
53
-