coze_lab 0.1.43 → 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.43",
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,230 +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
-
373
- creds = _load_credentials()
374
- if not creds or not creds.get("refresh_token"):
375
- hook_log("upload failure token refresh skipped; no local refresh_token")
376
- return None
377
-
378
- cached = creds.get("access_token")
379
- now_ms = int(time.time() * 1000)
380
- last_ms = int(_load_refresh_state().get("last_forced_refresh_ms", 0) or 0)
381
- if last_ms and now_ms - last_ms < _FORCE_REFRESH_COOLDOWN_MS:
382
- if cached and cached != current_token:
383
- hook_log("forced refresh throttled; reuse newer cached token")
384
- return cached
385
- hook_log(f"forced refresh throttled ageMs={now_ms - last_ms}")
386
- return None
387
-
388
- def _locked_refresh():
389
- locked_creds = _load_credentials()
390
- if not locked_creds or not locked_creds.get("refresh_token"):
391
- hook_log("forced refresh skipped in lock; credentials missing")
392
- return None
393
- locked_cached = locked_creds.get("access_token")
394
- locked_now_ms = int(time.time() * 1000)
395
- locked_last_ms = int(_load_refresh_state().get("last_forced_refresh_ms", 0) or 0)
396
- if locked_last_ms and locked_now_ms - locked_last_ms < _FORCE_REFRESH_COOLDOWN_MS:
397
- if locked_cached and locked_cached != current_token:
398
- hook_log("forced refresh already done by another process; reuse token")
399
- return locked_cached
400
- hook_log(f"forced refresh skipped in lock ageMs={locked_now_ms - locked_last_ms}")
401
- return None
402
- _save_refresh_state({
403
- "last_forced_refresh_ms": locked_now_ms,
404
- "last_reason": (reason or "upload_failure")[:120],
405
- })
406
- hook_log(f"forced token refresh after upload failure reason={(reason or 'upload_failure')[:120]}")
407
- refreshed = _refresh_token(locked_creds["refresh_token"])
408
- if refreshed:
409
- hook_log("forced token refresh OK")
410
- else:
411
- hook_log("forced token refresh FAILED")
412
- return refreshed
413
-
414
- return _with_refresh_lock(_locked_refresh)
415
-
416
- def _normalize_api_base_url(url: str) -> str:
417
- base = (url or "").strip().rstrip("/")
418
- if not base:
419
- return base
420
- # 剥除已知 loop 路径后缀,还原纯 API base。
421
- # 关键:含 /api 的变体必须连 /api 整体剥掉,否则残留 /api 会拼出 /api/v1/loop/... → 后端 400。
422
- # 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免误匹配短后缀。
423
- suffixes = (
424
- "/api/v1/loop/opentelemetry/v1/traces",
425
- _OTEL_SUFFIX + "/v1/traces",
426
- "/api/v1/loop/opentelemetry",
427
- _OTEL_SUFFIX,
428
- "/api/v1",
429
- "/v1",
430
- )
431
- for suffix in suffixes:
432
- if base.endswith(suffix):
433
- return base[:-len(suffix)].rstrip("/")
434
- return base
435
-
436
- def get_api_base_url() -> str:
437
- return _normalize_api_base_url(
438
- os.environ.get("COZELOOP_API_BASE_URL", "")
439
- )
440
-
441
- def _token_from_credentials() -> Optional[str]:
442
- creds = _load_credentials()
443
- if not creds:
444
- return None
445
- # expires_at 由 index.js 以毫秒存储;先减去当前毫秒时间再换算成秒,与 _REFRESH_THRESHOLD(秒) 比较。
446
- remaining = (creds.get("expires_at", 0) - time.time() * 1000) / 1000
447
- if remaining > _REFRESH_THRESHOLD:
448
- debug_log(f"Cached token valid, expires in {int(remaining)}s.")
449
- return creds.get("access_token")
450
- if creds.get("refresh_token"):
451
- debug_log(f"Token expiring in {int(remaining)}s, refreshing...")
452
- new_token = _refresh_token(creds["refresh_token"])
453
- if new_token:
454
- return new_token
455
- debug_log("Refresh failed.")
456
- return None
457
-
252
+ # --- Token resolution -----------------------------------------------------
458
253
 
459
254
  def get_fresh_token() -> Optional[str]:
460
- """Return a valid access token, refreshing if needed."""
255
+ """Return the token injected by onboard. Do not read local credentials."""
461
256
  env_token = os.environ.get("COZELOOP_API_TOKEN")
462
257
  env_coze_token = os.environ.get("COZE_API_TOKEN")
463
- is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
464
- if is_cloud:
465
- return env_token or env_coze_token or _token_from_credentials()
466
-
467
- # Local onboard used to write a short-lived access token into settings.local.json.
468
- # If credentials exist but cannot be refreshed, do not fall back to that stale
469
- # env token; failing closed keeps state from advancing on a known-bad token.
470
- creds = _load_credentials()
471
- if creds:
472
- return _token_from_credentials()
473
- if env_token and not env_coze_token:
474
- token = _token_from_credentials()
475
- if token:
476
- return token
477
- return env_token
478
258
  if env_token:
479
259
  return env_token
480
- if env_coze_token:
260
+ if os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes"):
481
261
  return env_coze_token
482
- return _token_from_credentials()
262
+ return None
483
263
 
484
264
  # -------------------------------------------------------------------------
485
265
 
@@ -1084,7 +864,7 @@ def _build_history_messages(history_turns: List[Dict[str, Any]]) -> list:
1084
864
 
1085
865
  # --- CozeLoop Trace Reporting ---
1086
866
 
1087
- 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):
1088
868
  """Send conversation turns to CozeLoop.
1089
869
 
1090
870
  Span hierarchy:
@@ -1107,14 +887,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1107
887
  print(f"[CozeLoop] Token 获取成功 ({token[:12]}...)", file=sys.stderr)
1108
888
  else:
1109
889
  print("[CozeLoop] 警告: 未找到有效 Token,上报可能失败", file=sys.stderr)
1110
- creds = _load_credentials()
1111
- # 云端模式:sandbox 注入的 COZELOOP_WORKSPACE_ID 必须优先于 credentials.json。
1112
- # 否则残留的本地 credentials.json workspace_id 会覆盖云端注入,导致 trace 上报错位。
1113
- is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
1114
- if is_cloud:
1115
- workspace_id = os.environ.get("COZELOOP_WORKSPACE_ID", "") or (creds or {}).get("workspace_id") or _DEFAULT_WORKSPACE_ID
1116
- else:
1117
- 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
1118
891
  os.environ["COZELOOP_WORKSPACE_ID"] = workspace_id
1119
892
  upload_events: List[str] = []
1120
893
  client_kwargs = {
@@ -1617,12 +1390,6 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
1617
1390
 
1618
1391
  if upload_events:
1619
1392
  debug_log(f"Upload failed, state not advanced. Last failure: {upload_events[-1][:500]}")
1620
- if retry_on_auth_failure:
1621
- new_token = force_refresh_token_after_upload_failure(upload_events[-1], token)
1622
- if new_token:
1623
- os.environ["COZELOOP_API_TOKEN"] = new_token
1624
- hook_log("retry upload once after forced token refresh")
1625
- return send_turns_to_cozeloop(turns, session_id, history_turns, retry_on_auth_failure=False, coze_tags_override=coze_tags_override)
1626
1393
  return None
1627
1394
 
1628
1395
  return True