coze_lab 0.1.23 → 0.1.25

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/README.md CHANGED
@@ -70,11 +70,12 @@ created after a new Codex turn, Codex did not load or execute the hook.
70
70
 
71
71
  OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
72
72
 
73
- In cloud mode, trace verification and hook uploads require
74
- `COZELOOP_API_TOKEN`. `COZE_API_TOKEN` is kept only as a legacy compatibility
75
- signal for hook injection; it is not treated as a CozeLoop SDK trace ingest
76
- token. If cloud only provides `COZE_API_TOKEN`, onboard still writes the hook
77
- configuration but reports `verify=fail` with `token_source=COZE_API_TOKEN`.
73
+ In cloud mode, trace verification and hook uploads prefer
74
+ `COZELOOP_API_TOKEN` and fall back to `COZE_API_TOKEN`. The selfcheck result is
75
+ authoritative: if the token does not have trace ingest permission, onboard still
76
+ writes the hook configuration but reports `verify=fail` with `token_source`.
77
+ For Python SDK uploads, `OTEL_ENDPOINT` is not used as the SDK base URL; set
78
+ `COZELOOP_API_BASE_URL` only when the SDK ingest endpoint should be overridden.
78
79
 
79
80
  **At hook execution time** (Claude Code / Codex), the Python hook script automatically:
80
81
  1. Reads `~/.cozeloop/credentials.json`
package/index.js CHANGED
@@ -51,7 +51,7 @@ function getCloudTokenInfo() {
51
51
  }
52
52
  const cozeToken = readEnv('COZE_API_TOKEN');
53
53
  if (cozeToken) {
54
- return { token: cozeToken, source: 'COZE_API_TOKEN', traceUsable: false };
54
+ return { token: cozeToken, source: 'COZE_API_TOKEN', traceUsable: true };
55
55
  }
56
56
  return { token: '', source: '', traceUsable: false };
57
57
  }
@@ -4706,6 +4706,10 @@ function normalizeTraceAgentIds(ids) {
4706
4706
 
4707
4707
  function getCloudCozeloopApiBaseUrl() {
4708
4708
  const raw = process.env.COZELOOP_API_BASE_URL || process.env.OTEL_ENDPOINT || '';
4709
+ return normalizeCozeloopApiBaseUrl(raw);
4710
+ }
4711
+
4712
+ function normalizeCozeloopApiBaseUrl(raw) {
4709
4713
  const base = raw.trim().replace(/\/+$/, '');
4710
4714
  if (!base) return '';
4711
4715
  if (base.endsWith('/v1/loop/opentelemetry/v1/traces')) {
@@ -4730,6 +4734,14 @@ function getCozeloopApiBaseUrl(cloud) {
4730
4734
  return cloud ? (getCloudCozeloopApiBaseUrl() || COZE_API) : COZE_API;
4731
4735
  }
4732
4736
 
4737
+ function getCloudCozeloopSdkApiBaseUrl() {
4738
+ return normalizeCozeloopApiBaseUrl(process.env.COZELOOP_API_BASE_URL || '');
4739
+ }
4740
+
4741
+ function getCozeloopSdkApiBaseUrl(cloud) {
4742
+ return cloud ? (getCloudCozeloopSdkApiBaseUrl() || COZE_API) : COZE_API;
4743
+ }
4744
+
4733
4745
  function getOtelEndpointBase(cloud) {
4734
4746
  return `${getCozeloopApiBaseUrl(cloud).replace(/\/+$/, '')}/v1/loop/opentelemetry`;
4735
4747
  }
@@ -4935,11 +4947,13 @@ function runCommand(input) {
4935
4947
 
4936
4948
  async function verifyTraceReportViaSdk(token, workspaceId, pairCode, pythonCmd, tokenSource) {
4937
4949
  const pair = pairCode || crypto.randomBytes(6).toString('hex');
4938
- const apiBase = getCozeloopApiBaseUrl(true);
4950
+ const apiBase = getCozeloopSdkApiBaseUrl(true);
4939
4951
  const script = `
4940
4952
  import json
4941
4953
  import os
4942
4954
  import sys
4955
+ import urllib.error
4956
+ import urllib.request
4943
4957
 
4944
4958
  events = []
4945
4959
 
@@ -4947,6 +4961,51 @@ def finish_event(info):
4947
4961
  if getattr(info, "is_event_fail", False):
4948
4962
  events.append(getattr(info, "detail_msg", "") or "trace export failed")
4949
4963
 
4964
+ def extract_logid(text):
4965
+ if not text:
4966
+ return ""
4967
+ marker = "logid="
4968
+ idx = text.find(marker)
4969
+ if idx >= 0:
4970
+ out = []
4971
+ for ch in text[idx + len(marker):]:
4972
+ if ch.isalnum():
4973
+ out.append(ch)
4974
+ else:
4975
+ break
4976
+ return "".join(out)
4977
+ for marker in ('"logid":"', '"log_id":"', '"Logid":"'):
4978
+ idx = text.find(marker)
4979
+ if idx >= 0:
4980
+ rest = text[idx + len(marker):]
4981
+ return rest.split('"', 1)[0]
4982
+ return ""
4983
+
4984
+ def http_diag():
4985
+ base = os.environ.get("COZELOOP_API_BASE_URL", "").strip().rstrip("/") or "https://api.coze.cn"
4986
+ url = base + "/v1/loop/traces/ingest"
4987
+ body = json.dumps({"spans": []}).encode()
4988
+ req = urllib.request.Request(
4989
+ url,
4990
+ data=body,
4991
+ headers={
4992
+ "Content-Type": "application/json",
4993
+ "Authorization": "Bearer " + os.environ.get("COZELOOP_API_TOKEN", ""),
4994
+ "x-tt-env": os.environ.get("x_tt_env", ""),
4995
+ "x-use-ppe": os.environ.get("x_use_ppe", ""),
4996
+ },
4997
+ method="POST",
4998
+ )
4999
+ try:
5000
+ with urllib.request.urlopen(req, timeout=10) as resp:
5001
+ text = resp.read().decode("utf-8", "replace")
5002
+ return {"url": url, "status": resp.status, "logid": resp.headers.get("x-tt-logid", "") or extract_logid(text), "body": text[:300]}
5003
+ except urllib.error.HTTPError as e:
5004
+ text = e.read().decode("utf-8", "replace")
5005
+ return {"url": url, "status": e.code, "logid": e.headers.get("x-tt-logid", "") or extract_logid(text), "body": text[:300]}
5006
+ except Exception as e:
5007
+ return {"url": url, "status": 0, "logid": "", "body": type(e).__name__ + ": " + str(e)}
5008
+
4950
5009
  try:
4951
5010
  import cozeloop
4952
5011
  kwargs = {
@@ -4966,11 +5025,15 @@ try:
4966
5025
  client.flush()
4967
5026
  client.close()
4968
5027
  if events:
4969
- print(json.dumps({"success": False, "body": "\\n".join(events), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
5028
+ diag = http_diag()
5029
+ body = "\\n".join(events) + "\\nhttp_diag=" + json.dumps(diag, ensure_ascii=False)
5030
+ print(json.dumps({"success": False, "body": body, "logid": diag.get("logid", ""), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
4970
5031
  sys.exit(1)
4971
- print(json.dumps({"success": True, "body": "", "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
5032
+ print(json.dumps({"success": True, "body": "", "logid": "", "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
4972
5033
  except Exception as e:
4973
- print(json.dumps({"success": False, "body": str(e), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
5034
+ diag = http_diag()
5035
+ body = str(e) + "\\nhttp_diag=" + json.dumps(diag, ensure_ascii=False)
5036
+ print(json.dumps({"success": False, "body": body, "logid": diag.get("logid", ""), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
4974
5037
  sys.exit(1)
4975
5038
  `;
4976
5039
  const env = {
@@ -5005,7 +5068,7 @@ except Exception as e:
5005
5068
  const snippet = String(body || '').slice(0, 300);
5006
5069
  if (snippet) console.log(snippet);
5007
5070
  }
5008
- return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource };
5071
+ return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || '' };
5009
5072
  }
5010
5073
 
5011
5074
  // 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
@@ -5420,32 +5483,28 @@ async function main() {
5420
5483
 
5421
5484
  // Step 1: Authorize.
5422
5485
  // 云端(--cloud):token 取自 sandbox 注入的环境变量,跳过 OAuth / credentials.json。
5423
- // trace 上报需要 COZELOOP_API_TOKEN;COZE_API_TOKEN 只保留为历史兼容来源,
5424
- // onboard 会写入 hook,但自检会明确标记它不是 CozeLoop ingest token。
5486
+ // 优先使用 COZELOOP_API_TOKEN;兼容使用 COZE_API_TOKEN,并以真实 selfcheck 为准。
5425
5487
  // 本地:load cached → refresh → device code。
5426
5488
  // 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
5427
5489
  let token;
5428
5490
  let tokenSource = '';
5429
- let cloudTraceUsableToken = true;
5430
5491
  if (args.cloud) {
5431
5492
  info('Step 1/5: 云端模式,从环境变量读取 trace token...');
5432
5493
  const tokenInfo = getCloudTokenInfo();
5433
5494
  token = tokenInfo.token;
5434
5495
  tokenSource = tokenInfo.source;
5435
- cloudTraceUsableToken = tokenInfo.traceUsable;
5436
5496
  if (!token) {
5437
5497
  errorBox([
5438
- 'ERROR: --cloud 模式要求环境变量 COZELOOP_API_TOKEN',
5498
+ 'ERROR: --cloud 模式要求环境变量 COZELOOP_API_TOKEN 或 COZE_API_TOKEN',
5439
5499
  '',
5440
- '云端 sandbox 应在进程环境中注入 COZELOOP_API_TOKEN。',
5500
+ '云端 sandbox 应在进程环境中注入可用于 trace ingest 的 token。',
5441
5501
  '未检测到该变量,无法配置 trace 上报。',
5442
5502
  ]);
5443
5503
  }
5444
5504
  cloudResult.token_source = tokenSource;
5445
- if (cloudTraceUsableToken) {
5446
- ok(`已从环境变量读取 ${tokenSource}`);
5447
- } else {
5448
- warn('仅检测到 COZE_API_TOKEN;它不是 CozeLoop SDK trace ingest token,自检预计会失败。');
5505
+ ok(`已从环境变量读取 ${tokenSource}`);
5506
+ if (tokenSource === 'COZE_API_TOKEN') {
5507
+ info('将兼容使用 COZE_API_TOKEN 作为 trace token,并通过 selfcheck 验证实际可用性。');
5449
5508
  }
5450
5509
  } else {
5451
5510
  info('Step 1/5: 检查授权状态...');
@@ -5534,30 +5593,15 @@ async function main() {
5534
5593
 
5535
5594
  // Step 5: Verify trace reporting end-to-end
5536
5595
  info('Step 5/5: 验证 trace 上报链路...');
5537
- let verifyResult;
5538
- if (args.cloud && !cloudTraceUsableToken) {
5539
- verifyResult = {
5540
- success: false,
5541
- status: 0,
5542
- body: '云端仅提供 COZE_API_TOKEN。Coze API token 不能作为 CozeLoop Python SDK 的 COZELOOP_API_TOKEN 上报 trace;请注入具备 CozeLoop trace ingest 权限的 COZELOOP_API_TOKEN。',
5543
- traceId: '',
5544
- pairCode: args.pairCode || '',
5545
- tokenSource,
5546
- apiBaseUrl: getCozeloopApiBaseUrl(true),
5547
- };
5548
- warn('trace 上报跳过:缺少 COZELOOP_API_TOKEN。');
5549
- console.log(verifyResult.body);
5550
- } else {
5551
- verifyResult = args.cloud
5552
- ? await verifyTraceReportViaSdk(token, WORKSPACE_ID, args.pairCode, pythonCmd || 'python3', tokenSource)
5553
- : await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
5554
- }
5596
+ const verifyResult = args.cloud
5597
+ ? await verifyTraceReportViaSdk(token, WORKSPACE_ID, args.pairCode, pythonCmd || 'python3', tokenSource)
5598
+ : await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
5555
5599
  if (verifyResult.success) {
5556
5600
  cloudResult.verify = 'ok';
5557
5601
  } else if (CLOUD_MODE) {
5558
5602
  // 云端:注入已成功,验证失败不阻断(放行),记录结果供后台弹 warning。
5559
5603
  cloudResult.verify = 'fail';
5560
- cloudResult.logid = extractLogid(verifyResult.body) || cloudResult.logid;
5604
+ cloudResult.logid = verifyResult.logid || extractLogid(verifyResult.body) || cloudResult.logid;
5561
5605
  cloudResult.message = `trace 上报自检失败 HTTP ${verifyResult.status}: ${(verifyResult.body || '').slice(0, 200)}`;
5562
5606
  warn('trace 上报自检失败,但 hook 配置已写入(云端放行)。');
5563
5607
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -264,7 +264,7 @@ def _normalize_api_base_url(url: str) -> str:
264
264
 
265
265
  def get_api_base_url() -> str:
266
266
  return _normalize_api_base_url(
267
- os.environ.get("COZELOOP_API_BASE_URL", "") or os.environ.get("OTEL_ENDPOINT", "")
267
+ os.environ.get("COZELOOP_API_BASE_URL", "")
268
268
  )
269
269
 
270
270
  def get_fresh_token() -> Optional[str]:
@@ -273,6 +273,8 @@ def get_fresh_token() -> Optional[str]:
273
273
  env_coze_token = os.environ.get("COZE_API_TOKEN")
274
274
  if env_token:
275
275
  return env_token
276
+ if env_coze_token:
277
+ return env_coze_token
276
278
  creds = _load_credentials()
277
279
  if creds:
278
280
  expires_at_sec = creds.get("expires_at", 0) / 1000
@@ -286,8 +288,6 @@ def get_fresh_token() -> Optional[str]:
286
288
  if new_token:
287
289
  return new_token
288
290
  debug_log("Refresh failed, falling back to env var.")
289
- if env_coze_token:
290
- debug_log("COZE_API_TOKEN present but ignored; CozeLoop trace upload requires COZELOOP_API_TOKEN")
291
291
  return None
292
292
 
293
293
  # -------------------------------------------------------------------------
@@ -225,7 +225,7 @@ def _normalize_api_base_url(url: str) -> str:
225
225
 
226
226
  def get_api_base_url() -> str:
227
227
  return _normalize_api_base_url(
228
- os.environ.get("COZELOOP_API_BASE_URL", "") or os.environ.get("OTEL_ENDPOINT", "")
228
+ os.environ.get("COZELOOP_API_BASE_URL", "")
229
229
  )
230
230
 
231
231
 
@@ -234,6 +234,8 @@ def get_fresh_token():
234
234
  env_coze_token = os.environ.get("COZE_API_TOKEN")
235
235
  if env_token:
236
236
  return env_token
237
+ if env_coze_token:
238
+ return env_coze_token
237
239
  creds = _load_credentials()
238
240
  if creds:
239
241
  remaining = creds.get("expires_at", 0) / 1000 - time.time()
@@ -243,8 +245,6 @@ def get_fresh_token():
243
245
  new_token = _refresh_token(creds["refresh_token"])
244
246
  if new_token:
245
247
  return new_token
246
- if env_coze_token:
247
- hook_log("COZE_API_TOKEN present but ignored; CozeLoop trace upload requires COZELOOP_API_TOKEN")
248
248
  return None
249
249
  # -------------------------------------------------------------------------
250
250