coze_lab 0.1.22 → 0.1.23

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
@@ -24,7 +24,7 @@ npx coze_lab --logout # Clear cached credentials
24
24
  |-----------|----------|-----------------|
25
25
  | `--agent` | ✓ (for setup) | `claude-code`, `codex`, `openclaw` |
26
26
  | `--agent-id` | — | Resolve `~/.coze/agents/<agentId>/config.json` and write per-agent config |
27
- | `--cloud` | — | Cloud mode: read token from env and emit `COZE_LAB_RESULT=...` |
27
+ | `--cloud` | — | Cloud mode: read trace token from env and emit `COZE_LAB_RESULT=...` |
28
28
  | `--codex-home` | — | Override Codex config home for non-cloud/custom runs |
29
29
  | `--login` | — | Run the Device Code login flow only |
30
30
  | `--status` | — | Print local token status (valid / expiring / expired) |
@@ -70,6 +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`.
78
+
73
79
  **At hook execution time** (Claude Code / Codex), the Python hook script automatically:
74
80
  1. Reads `~/.cozeloop/credentials.json`
75
81
  2. If the token expires in < 10 minutes, calls the Coze refresh API
package/index.js CHANGED
@@ -17,7 +17,7 @@ const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
17
17
  // 云端(--cloud)模式:在 stdout 输出一行机器可读结果 COZE_LAB_RESULT={...},
18
18
  // 供管理后台解析判定(inject/verify/logid/message),不依赖中文文案。
19
19
  let CLOUD_MODE = false;
20
- const cloudResult = { version: PACKAGE_VERSION, inject: 'skip', verify: 'skip', logid: '', message: '' };
20
+ const cloudResult = { version: PACKAGE_VERSION, inject: 'skip', verify: 'skip', logid: '', message: '', token_source: '' };
21
21
 
22
22
  // errorBox 在云端模式下抛此异常(而非 process.exit),由 main 外层统一收尾。
23
23
  class CloudAbort extends Error {}
@@ -40,6 +40,22 @@ function emitCloudResult() {
40
40
  console.log('COZE_LAB_RESULT=' + JSON.stringify(safe));
41
41
  }
42
42
 
43
+ function readEnv(name) {
44
+ return String(process.env[name] || '').trim();
45
+ }
46
+
47
+ function getCloudTokenInfo() {
48
+ const loopToken = readEnv('COZELOOP_API_TOKEN');
49
+ if (loopToken) {
50
+ return { token: loopToken, source: 'COZELOOP_API_TOKEN', traceUsable: true };
51
+ }
52
+ const cozeToken = readEnv('COZE_API_TOKEN');
53
+ if (cozeToken) {
54
+ return { token: cozeToken, source: 'COZE_API_TOKEN', traceUsable: false };
55
+ }
56
+ return { token: '', source: '', traceUsable: false };
57
+ }
58
+
43
59
  // ─── 1. Color helpers ────────────────────────────────────────────────────────
44
60
  const C = {
45
61
  reset: '\x1b[0m',
@@ -4526,22 +4542,26 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
4526
4542
  if (!existing.env) existing.env = {};
4527
4543
  existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
4528
4544
  if (cloud) {
4529
- if (process.env.COZELOOP_API_TOKEN) {
4530
- existing.env.COZELOOP_API_TOKEN = process.env.COZELOOP_API_TOKEN;
4545
+ const loopToken = readEnv('COZELOOP_API_TOKEN');
4546
+ const cozeToken = readEnv('COZE_API_TOKEN');
4547
+ if (loopToken) {
4548
+ existing.env.COZELOOP_API_TOKEN = loopToken;
4531
4549
  delete existing.env.COZE_API_TOKEN;
4532
- } else if (process.env.COZE_API_TOKEN) {
4533
- existing.env.COZE_API_TOKEN = process.env.COZE_API_TOKEN;
4550
+ } else if (cozeToken) {
4551
+ existing.env.COZE_API_TOKEN = cozeToken;
4534
4552
  delete existing.env.COZELOOP_API_TOKEN;
4535
4553
  }
4536
4554
  } else {
4537
4555
  existing.env.COZELOOP_API_TOKEN = patToken;
4538
4556
  delete existing.env.COZE_API_TOKEN;
4539
4557
  }
4540
- if (process.env.COZELOOP_API_BASE_URL) {
4541
- existing.env.COZELOOP_API_BASE_URL = process.env.COZELOOP_API_BASE_URL;
4558
+ const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
4559
+ const otelEndpoint = readEnv('OTEL_ENDPOINT');
4560
+ if (loopBaseUrl) {
4561
+ existing.env.COZELOOP_API_BASE_URL = loopBaseUrl;
4542
4562
  delete existing.env.OTEL_ENDPOINT;
4543
- } else if (process.env.OTEL_ENDPOINT) {
4544
- existing.env.OTEL_ENDPOINT = process.env.OTEL_ENDPOINT;
4563
+ } else if (otelEndpoint) {
4564
+ existing.env.OTEL_ENDPOINT = otelEndpoint;
4545
4565
  delete existing.env.COZELOOP_API_BASE_URL;
4546
4566
  }
4547
4567
  // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
@@ -4584,10 +4604,12 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4584
4604
  shellEnvLine('COZELOOP_WORKSPACE_ID', workspaceId),
4585
4605
  ];
4586
4606
  if (cloud) {
4587
- if (process.env.COZELOOP_API_TOKEN) {
4588
- envLines.push(shellEnvLine('COZELOOP_API_TOKEN', process.env.COZELOOP_API_TOKEN));
4589
- } else if (process.env.COZE_API_TOKEN) {
4590
- envLines.push(shellEnvLine('COZE_API_TOKEN', process.env.COZE_API_TOKEN));
4607
+ const loopToken = readEnv('COZELOOP_API_TOKEN');
4608
+ const cozeToken = readEnv('COZE_API_TOKEN');
4609
+ if (loopToken) {
4610
+ envLines.push(shellEnvLine('COZELOOP_API_TOKEN', loopToken));
4611
+ } else if (cozeToken) {
4612
+ envLines.push(shellEnvLine('COZE_API_TOKEN', cozeToken));
4591
4613
  }
4592
4614
  } else {
4593
4615
  envLines.push(shellEnvLine('COZELOOP_API_TOKEN', token));
@@ -4595,10 +4617,12 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4595
4617
  envLines.push(shellEnvLine('CODEX_HOME', home));
4596
4618
  envLines.push(shellEnvLine('COZELOOP_HOOK_LOG', logFile));
4597
4619
  envLines.push('TRACE_TO_COZELOOP=true');
4598
- if (process.env.COZELOOP_API_BASE_URL) {
4599
- envLines.push(shellEnvLine('COZELOOP_API_BASE_URL', process.env.COZELOOP_API_BASE_URL));
4600
- } else if (process.env.OTEL_ENDPOINT) {
4601
- envLines.push(shellEnvLine('OTEL_ENDPOINT', process.env.OTEL_ENDPOINT));
4620
+ const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
4621
+ const otelEndpoint = readEnv('OTEL_ENDPOINT');
4622
+ if (loopBaseUrl) {
4623
+ envLines.push(shellEnvLine('COZELOOP_API_BASE_URL', loopBaseUrl));
4624
+ } else if (otelEndpoint) {
4625
+ envLines.push(shellEnvLine('OTEL_ENDPOINT', otelEndpoint));
4602
4626
  }
4603
4627
  // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
4604
4628
  envLines.push(shellEnvLine('x_tt_env', PPE_TT_ENV));
@@ -4909,8 +4933,9 @@ function runCommand(input) {
4909
4933
  });
4910
4934
  }
4911
4935
 
4912
- async function verifyTraceReportViaSdk(token, workspaceId, pairCode, pythonCmd) {
4936
+ async function verifyTraceReportViaSdk(token, workspaceId, pairCode, pythonCmd, tokenSource) {
4913
4937
  const pair = pairCode || crypto.randomBytes(6).toString('hex');
4938
+ const apiBase = getCozeloopApiBaseUrl(true);
4914
4939
  const script = `
4915
4940
  import json
4916
4941
  import os
@@ -4941,11 +4966,11 @@ try:
4941
4966
  client.flush()
4942
4967
  client.close()
4943
4968
  if events:
4944
- print(json.dumps({"success": False, "body": "\\n".join(events)}, ensure_ascii=False))
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))
4945
4970
  sys.exit(1)
4946
- print(json.dumps({"success": True, "body": ""}, ensure_ascii=False))
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))
4947
4972
  except Exception as e:
4948
- print(json.dumps({"success": False, "body": str(e)}, ensure_ascii=False))
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))
4949
4974
  sys.exit(1)
4950
4975
  `;
4951
4976
  const env = {
@@ -4953,10 +4978,10 @@ except Exception as e:
4953
4978
  COZELOOP_WORKSPACE_ID: workspaceId,
4954
4979
  COZELOOP_API_TOKEN: token,
4955
4980
  COZELOOP_PAIR_CODE: pair,
4981
+ COZELOOP_TOKEN_SOURCE: tokenSource || '',
4956
4982
  x_tt_env: PPE_TT_ENV,
4957
4983
  x_use_ppe: PPE_USE_PPE,
4958
4984
  };
4959
- const apiBase = getCozeloopApiBaseUrl(true);
4960
4985
  if (apiBase) env.COZELOOP_API_BASE_URL = apiBase;
4961
4986
  const result = await runCommand({
4962
4987
  cmd: pythonCmd || 'python3',
@@ -4976,10 +5001,11 @@ except Exception as e:
4976
5001
  info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
4977
5002
  } else {
4978
5003
  warn(`trace 上报失败: SDK selfcheck exit ${result.code}`);
5004
+ info(`trace selfcheck api_base_url=${apiBase || COZE_API}, token_source=${tokenSource || 'unknown'}`);
4979
5005
  const snippet = String(body || '').slice(0, 300);
4980
5006
  if (snippet) console.log(snippet);
4981
5007
  }
4982
- return { success, status: result.code || 0, body, traceId: '', pairCode: pair };
5008
+ return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource };
4983
5009
  }
4984
5010
 
4985
5011
  // 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
@@ -5394,25 +5420,37 @@ async function main() {
5394
5420
 
5395
5421
  // Step 1: Authorize.
5396
5422
  // 云端(--cloud):token 取自 sandbox 注入的环境变量,跳过 OAuth / credentials.json。
5397
- // 云端真实变量名是 COZELOOP_API_TOKEN(兼容历史的 COZE_API_TOKEN)。
5423
+ // trace 上报需要 COZELOOP_API_TOKEN;COZE_API_TOKEN 只保留为历史兼容来源,
5424
+ // onboard 会写入 hook,但自检会明确标记它不是 CozeLoop ingest token。
5398
5425
  // 本地:load cached → refresh → device code。
5399
5426
  // 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
5400
5427
  let token;
5428
+ let tokenSource = '';
5429
+ let cloudTraceUsableToken = true;
5401
5430
  if (args.cloud) {
5402
- info('Step 1/5: 云端模式,从环境变量读取 COZELOOP_API_TOKEN...');
5403
- token = process.env.COZELOOP_API_TOKEN || process.env.COZE_API_TOKEN;
5431
+ info('Step 1/5: 云端模式,从环境变量读取 trace token...');
5432
+ const tokenInfo = getCloudTokenInfo();
5433
+ token = tokenInfo.token;
5434
+ tokenSource = tokenInfo.source;
5435
+ cloudTraceUsableToken = tokenInfo.traceUsable;
5404
5436
  if (!token) {
5405
5437
  errorBox([
5406
5438
  'ERROR: --cloud 模式要求环境变量 COZELOOP_API_TOKEN',
5407
5439
  '',
5408
- '云端 sandbox 应在进程环境中注入 COZELOOP_API_TOKEN(或 COZE_API_TOKEN)。',
5440
+ '云端 sandbox 应在进程环境中注入 COZELOOP_API_TOKEN',
5409
5441
  '未检测到该变量,无法配置 trace 上报。',
5410
5442
  ]);
5411
5443
  }
5412
- ok('已从环境变量读取 COZELOOP_API_TOKEN');
5444
+ cloudResult.token_source = tokenSource;
5445
+ if (cloudTraceUsableToken) {
5446
+ ok(`已从环境变量读取 ${tokenSource}`);
5447
+ } else {
5448
+ warn('仅检测到 COZE_API_TOKEN;它不是 CozeLoop SDK trace ingest token,自检预计会失败。');
5449
+ }
5413
5450
  } else {
5414
5451
  info('Step 1/5: 检查授权状态...');
5415
5452
  token = await getValidToken();
5453
+ tokenSource = 'credentials';
5416
5454
  }
5417
5455
  console.log('');
5418
5456
 
@@ -5496,9 +5534,24 @@ async function main() {
5496
5534
 
5497
5535
  // Step 5: Verify trace reporting end-to-end
5498
5536
  info('Step 5/5: 验证 trace 上报链路...');
5499
- const verifyResult = args.cloud
5500
- ? await verifyTraceReportViaSdk(token, WORKSPACE_ID, args.pairCode, pythonCmd || 'python3')
5501
- : await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
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
+ }
5502
5555
  if (verifyResult.success) {
5503
5556
  cloudResult.verify = 'ok';
5504
5557
  } else if (CLOUD_MODE) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -269,11 +269,10 @@ def get_api_base_url() -> str:
269
269
 
270
270
  def get_fresh_token() -> Optional[str]:
271
271
  """Return a valid access token, refreshing if needed. Falls back to env var."""
272
- api_base_url = get_api_base_url()
273
272
  env_token = os.environ.get("COZELOOP_API_TOKEN")
274
273
  env_coze_token = os.environ.get("COZE_API_TOKEN")
275
- if api_base_url and (env_token or env_coze_token):
276
- return env_token or env_coze_token
274
+ if env_token:
275
+ return env_token
277
276
  creds = _load_credentials()
278
277
  if creds:
279
278
  expires_at_sec = creds.get("expires_at", 0) / 1000
@@ -287,12 +286,8 @@ def get_fresh_token() -> Optional[str]:
287
286
  if new_token:
288
287
  return new_token
289
288
  debug_log("Refresh failed, falling back to env var.")
290
- if env_token:
291
- return env_token
292
- if api_base_url:
293
- return env_coze_token
294
289
  if env_coze_token:
295
- debug_log("COZE_API_TOKEN present but ignored without COZELOOP_API_BASE_URL or OTEL_ENDPOINT")
290
+ debug_log("COZE_API_TOKEN present but ignored; CozeLoop trace upload requires COZELOOP_API_TOKEN")
296
291
  return None
297
292
 
298
293
  # -------------------------------------------------------------------------
@@ -230,11 +230,10 @@ def get_api_base_url() -> str:
230
230
 
231
231
 
232
232
  def get_fresh_token():
233
- api_base_url = get_api_base_url()
234
233
  env_token = os.environ.get("COZELOOP_API_TOKEN")
235
234
  env_coze_token = os.environ.get("COZE_API_TOKEN")
236
- if api_base_url and (env_token or env_coze_token):
237
- return env_token or env_coze_token
235
+ if env_token:
236
+ return env_token
238
237
  creds = _load_credentials()
239
238
  if creds:
240
239
  remaining = creds.get("expires_at", 0) / 1000 - time.time()
@@ -244,12 +243,8 @@ def get_fresh_token():
244
243
  new_token = _refresh_token(creds["refresh_token"])
245
244
  if new_token:
246
245
  return new_token
247
- if env_token:
248
- return env_token
249
- if api_base_url:
250
- return env_coze_token
251
246
  if env_coze_token:
252
- hook_log("COZE_API_TOKEN present but ignored without COZELOOP_API_BASE_URL or OTEL_ENDPOINT")
247
+ hook_log("COZE_API_TOKEN present but ignored; CozeLoop trace upload requires COZELOOP_API_TOKEN")
253
248
  return None
254
249
  # -------------------------------------------------------------------------
255
250