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/index.js +879 -83
- package/package.json +4 -1
- package/scripts/claude-code/cozeloop_hook.py +56 -19
- package/scripts/codex/cozeloop_hook.py +47 -14
- package/scripts/openclaw/dist/cozeloop-exporter.js +11 -3
- package/scripts/openclaw/dist/index.js +1 -0
- package/scripts/openclaw/openclaw.plugin.json +6 -1
- package/scripts/shared/cozeloop_refresh.py +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coze_lab",
|
|
3
|
-
"version": "0.1.
|
|
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={
|
|
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.
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
|
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.
|
|
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={
|
|
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
|
-
|