coze_lab 0.1.44 → 0.1.45

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.44",
3
+ "version": "0.1.45",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -21,7 +21,6 @@
21
21
  "index.js",
22
22
  "scripts/claude-code/cozeloop_hook.py",
23
23
  "scripts/codex/cozeloop_hook.py",
24
- "scripts/shared/cozeloop_refresh.py",
25
24
  "scripts/openclaw/dist/",
26
25
  "scripts/openclaw/openclaw.plugin.json",
27
26
  "scripts/openclaw/package.json"
@@ -22,8 +22,6 @@ import sys
22
22
  import glob
23
23
  import hashlib
24
24
  import time
25
- import urllib.request
26
- import urllib.error
27
25
  from datetime import datetime
28
26
  from pathlib import Path
29
27
  from typing import Optional, List, Dict, Any
@@ -105,12 +103,7 @@ else:
105
103
 
106
104
  # --- Configuration ---
107
105
  DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
108
- _COZELOOP_CLIENT_ID = "08972682140163281554629748278108.app.coze"
109
- _COZE_API = "https://api.coze.cn"
110
106
  _OTEL_SUFFIX = "/v1/loop/opentelemetry"
111
- _REFRESH_THRESHOLD = 10 * 60 # refresh when < 10 minutes remain
112
- _FORCE_REFRESH_COOLDOWN_MS = 60 * 1000
113
- _REFRESH_LOCK_STALE = 30
114
107
  _DEFAULT_WORKSPACE_ID = "7649231955045072915" # hardcoded spaceID fallback
115
108
 
116
109
 
@@ -256,235 +249,17 @@ def debug_log(message: str):
256
249
  if DEBUG:
257
250
  print(f"[COZELOOP_HOOK_DEBUG] {datetime.now().isoformat()} - {message}", file=sys.stderr)
258
251
 
259
- # --- Token refresh --------------------------------------------------------
260
-
261
- def _get_credentials_path() -> Path:
262
- return Path.home() / ".cozeloop" / "credentials.json"
263
-
264
- def _get_refresh_state_path() -> Path:
265
- return Path.home() / ".cozeloop" / "refresh-state.json"
266
-
267
- def _get_refresh_lock_path() -> Path:
268
- return Path.home() / ".cozeloop" / "refresh-state.lock"
269
-
270
- def _load_credentials() -> Optional[Dict]:
271
- path = _get_credentials_path()
272
- if not path.exists():
273
- return None
274
- try:
275
- return json.loads(path.read_text())
276
- except Exception:
277
- return None
278
-
279
- def _save_credentials(creds: Dict):
280
- path = _get_credentials_path()
281
- path.parent.mkdir(parents=True, exist_ok=True)
282
- path.write_text(json.dumps(creds, indent=2))
283
- os.chmod(path, 0o600)
284
-
285
- def _load_refresh_state() -> Dict[str, Any]:
286
- path = _get_refresh_state_path()
287
- if not path.exists():
288
- return {}
289
- try:
290
- return json.loads(path.read_text())
291
- except Exception:
292
- return {}
293
-
294
- def _save_refresh_state(patch: Dict[str, Any]):
295
- path = _get_refresh_state_path()
296
- try:
297
- path.parent.mkdir(parents=True, exist_ok=True)
298
- state = _load_refresh_state()
299
- state.update(patch)
300
- path.write_text(json.dumps(state, indent=2))
301
- os.chmod(path, 0o600)
302
- except Exception:
303
- pass
304
-
305
- def _with_refresh_lock(fn):
306
- lock_path = _get_refresh_lock_path()
307
- lock_path.parent.mkdir(parents=True, exist_ok=True)
308
- for _ in range(24):
309
- fd = None
310
- try:
311
- fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
312
- try:
313
- return fn()
314
- finally:
315
- try:
316
- os.close(fd)
317
- except Exception:
318
- pass
319
- try:
320
- lock_path.unlink()
321
- except Exception:
322
- pass
323
- except FileExistsError:
324
- try:
325
- if time.time() - lock_path.stat().st_mtime > _REFRESH_LOCK_STALE:
326
- lock_path.unlink()
327
- continue
328
- except Exception:
329
- pass
330
- time.sleep(0.25)
331
- hook_log("forced refresh skipped because refresh lock is busy")
332
- return None
333
-
334
- def _refresh_token(refresh_token: str) -> Optional[str]:
335
- """Call Coze refresh token API. Returns new access_token or None on failure."""
336
- try:
337
- payload = json.dumps({
338
- "grant_type": "refresh_token",
339
- "client_id": _COZELOOP_CLIENT_ID,
340
- "refresh_token": refresh_token,
341
- }).encode()
342
- req = urllib.request.Request(
343
- f"{_COZE_API}/api/permission/oauth2/token",
344
- data=payload,
345
- headers={
346
- "Content-Type": "application/json",
347
- },
348
- )
349
- with urllib.request.urlopen(req, timeout=10) as resp:
350
- data = json.loads(resp.read())
351
- if data.get("access_token"):
352
- existing = _load_credentials() or {}
353
- creds = {
354
- "access_token": data["access_token"],
355
- "refresh_token": data.get("refresh_token", refresh_token),
356
- "expires_at": data.get("expires_in", 0) * 1000, # unix timestamp in seconds
357
- "workspace_id": existing.get("workspace_id", ""),
358
- }
359
- _save_credentials(creds)
360
- debug_log("Token refreshed successfully.")
361
- return creds["access_token"]
362
- except Exception as e:
363
- debug_log(f"Token refresh failed: {e}")
364
- return None
365
-
366
- def force_refresh_token_after_upload_failure(reason: str = "upload_failure", current_token: Optional[str] = None) -> Optional[str]:
367
- """Force-refresh local credentials after an upload failure, with cross-process throttling."""
368
- is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
369
- if is_cloud:
370
- hook_log("upload failure token refresh skipped in cloud mode")
371
- return None
372
- if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken":
373
- hook_log("upload failure token refresh skipped for agent config patToken")
374
- return None
375
-
376
- creds = _load_credentials()
377
- if not creds or not creds.get("refresh_token"):
378
- hook_log("upload failure token refresh skipped; no local refresh_token")
379
- return None
380
-
381
- cached = creds.get("access_token")
382
- now_ms = int(time.time() * 1000)
383
- last_ms = int(_load_refresh_state().get("last_forced_refresh_ms", 0) or 0)
384
- if last_ms and now_ms - last_ms < _FORCE_REFRESH_COOLDOWN_MS:
385
- if cached and cached != current_token:
386
- hook_log("forced refresh throttled; reuse newer cached token")
387
- return cached
388
- hook_log(f"forced refresh throttled ageMs={now_ms - last_ms}")
389
- return None
390
-
391
- def _locked_refresh():
392
- locked_creds = _load_credentials()
393
- if not locked_creds or not locked_creds.get("refresh_token"):
394
- hook_log("forced refresh skipped in lock; credentials missing")
395
- return None
396
- locked_cached = locked_creds.get("access_token")
397
- locked_now_ms = int(time.time() * 1000)
398
- locked_last_ms = int(_load_refresh_state().get("last_forced_refresh_ms", 0) or 0)
399
- if locked_last_ms and locked_now_ms - locked_last_ms < _FORCE_REFRESH_COOLDOWN_MS:
400
- if locked_cached and locked_cached != current_token:
401
- hook_log("forced refresh already done by another process; reuse token")
402
- return locked_cached
403
- hook_log(f"forced refresh skipped in lock ageMs={locked_now_ms - locked_last_ms}")
404
- return None
405
- _save_refresh_state({
406
- "last_forced_refresh_ms": locked_now_ms,
407
- "last_reason": (reason or "upload_failure")[:120],
408
- })
409
- hook_log(f"forced token refresh after upload failure reason={(reason or 'upload_failure')[:120]}")
410
- refreshed = _refresh_token(locked_creds["refresh_token"])
411
- if refreshed:
412
- hook_log("forced token refresh OK")
413
- else:
414
- hook_log("forced token refresh FAILED")
415
- return refreshed
416
-
417
- return _with_refresh_lock(_locked_refresh)
418
-
419
- def _normalize_api_base_url(url: str) -> str:
420
- base = (url or "").strip().rstrip("/")
421
- if not base:
422
- return base
423
- # 剥除已知 loop 路径后缀,还原纯 API base。
424
- # 关键:含 /api 的变体必须连 /api 整体剥掉,否则残留 /api 会拼出 /api/v1/loop/... → 后端 400。
425
- # 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免误匹配短后缀。
426
- suffixes = (
427
- "/api/v1/loop/opentelemetry/v1/traces",
428
- _OTEL_SUFFIX + "/v1/traces",
429
- "/api/v1/loop/opentelemetry",
430
- _OTEL_SUFFIX,
431
- "/api/v1",
432
- "/v1",
433
- )
434
- for suffix in suffixes:
435
- if base.endswith(suffix):
436
- return base[:-len(suffix)].rstrip("/")
437
- return base
438
-
439
- def get_api_base_url() -> str:
440
- return _normalize_api_base_url(
441
- os.environ.get("COZELOOP_API_BASE_URL", "")
442
- )
443
-
444
- def _token_from_credentials() -> Optional[str]:
445
- creds = _load_credentials()
446
- if not creds:
447
- return None
448
- # expires_at 由 index.js 以毫秒存储;先减去当前毫秒时间再换算成秒,与 _REFRESH_THRESHOLD(秒) 比较。
449
- remaining = (creds.get("expires_at", 0) - time.time() * 1000) / 1000
450
- if remaining > _REFRESH_THRESHOLD:
451
- debug_log(f"Cached token valid, expires in {int(remaining)}s.")
452
- return creds.get("access_token")
453
- if creds.get("refresh_token"):
454
- debug_log(f"Token expiring in {int(remaining)}s, refreshing...")
455
- new_token = _refresh_token(creds["refresh_token"])
456
- if new_token:
457
- return new_token
458
- debug_log("Refresh failed.")
459
- return None
460
-
252
+ # --- Token resolution -----------------------------------------------------
461
253
 
462
254
  def get_fresh_token() -> Optional[str]:
463
- """Return a valid access token, refreshing if needed."""
255
+ """Return the token injected by onboard. Do not read local credentials."""
464
256
  env_token = os.environ.get("COZELOOP_API_TOKEN")
465
257
  env_coze_token = os.environ.get("COZE_API_TOKEN")
466
- is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
467
- if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken" and env_token:
468
- return env_token
469
- if is_cloud:
470
- return env_token or env_coze_token or _token_from_credentials()
471
-
472
- # Local onboard used to write a short-lived access token into settings.local.json.
473
- # If credentials exist but cannot be refreshed, do not fall back to that stale
474
- # env token; failing closed keeps state from advancing on a known-bad token.
475
- creds = _load_credentials()
476
- if creds:
477
- return _token_from_credentials()
478
- if env_token and not env_coze_token:
479
- token = _token_from_credentials()
480
- if token:
481
- return token
482
- return env_token
483
258
  if env_token:
484
259
  return env_token
485
- if env_coze_token:
260
+ if os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes"):
486
261
  return env_coze_token
487
- return _token_from_credentials()
262
+ return None
488
263
 
489
264
  # -------------------------------------------------------------------------
490
265
 
@@ -1089,7 +864,7 @@ def _build_history_messages(history_turns: List[Dict[str, Any]]) -> list:
1089
864
 
1090
865
  # --- CozeLoop Trace Reporting ---
1091
866
 
1092
- def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history_turns: Optional[List[Dict[str, Any]]] = None, retry_on_auth_failure: bool = True, coze_tags_override: Optional[Dict[str, str]] = None):
867
+ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history_turns: Optional[List[Dict[str, Any]]] = None, coze_tags_override: Optional[Dict[str, str]] = None):
1093
868
  """Send conversation turns to CozeLoop.
1094
869
 
1095
870
  Span hierarchy:
@@ -1112,14 +887,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1112
887
  print(f"[CozeLoop] Token 获取成功 ({token[:12]}...)", file=sys.stderr)
1113
888
  else:
1114
889
  print("[CozeLoop] 警告: 未找到有效 Token,上报可能失败", file=sys.stderr)
1115
- creds = _load_credentials()
1116
- # 云端模式:sandbox 注入的 COZELOOP_WORKSPACE_ID 必须优先于 credentials.json。
1117
- # 否则残留的本地 credentials.json workspace_id 会覆盖云端注入,导致 trace 上报错位。
1118
- is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
1119
- if is_cloud:
1120
- workspace_id = os.environ.get("COZELOOP_WORKSPACE_ID", "") or (creds or {}).get("workspace_id") or _DEFAULT_WORKSPACE_ID
1121
- else:
1122
- workspace_id = (creds or {}).get("workspace_id") or os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
890
+ workspace_id = os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
1123
891
  os.environ["COZELOOP_WORKSPACE_ID"] = workspace_id
1124
892
  upload_events: List[str] = []
1125
893
  client_kwargs = {
@@ -1622,12 +1390,6 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1622
1390
 
1623
1391
  if upload_events:
1624
1392
  debug_log(f"Upload failed, state not advanced. Last failure: {upload_events[-1][:500]}")
1625
- if retry_on_auth_failure:
1626
- new_token = force_refresh_token_after_upload_failure(upload_events[-1], token)
1627
- if new_token:
1628
- os.environ["COZELOOP_API_TOKEN"] = new_token
1629
- hook_log("retry upload once after forced token refresh")
1630
- return send_turns_to_cozeloop(turns, session_id, history_turns, retry_on_auth_failure=False, coze_tags_override=coze_tags_override)
1631
1393
  return None
1632
1394
 
1633
1395
  return True