coze_lab 0.1.21 → 0.1.22

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 CHANGED
@@ -4472,8 +4472,6 @@ function shellEnvLine(key, value) {
4472
4472
  // writeClaudeCodeHook 配置 Claude Code 的 hook。
4473
4473
  // configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
4474
4474
  // settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
4475
- // cloud=true 时不把明文 token 写进 settings.local.json —— 云端 hook 运行时直接读
4476
- // 环境变量 COZE_API_TOKEN(见 scripts/claude-code/cozeloop_hook.py 的 get_fresh_token)。
4477
4475
  function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cloud) {
4478
4476
  const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
4479
4477
  const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
@@ -4527,12 +4525,24 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
4527
4525
  const localSettings = mergeJson(localSettingsPath, (existing) => {
4528
4526
  if (!existing.env) existing.env = {};
4529
4527
  existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
4530
- // 云端:token 由 sandbox 注入到环境变量 COZE_API_TOKEN,hook 运行时直接读,
4531
- // 不在配置文件落明文 token。本地:写入 OAuth 拿到的 token。
4532
4528
  if (cloud) {
4533
- delete existing.env.COZELOOP_API_TOKEN;
4529
+ if (process.env.COZELOOP_API_TOKEN) {
4530
+ existing.env.COZELOOP_API_TOKEN = process.env.COZELOOP_API_TOKEN;
4531
+ 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;
4534
+ delete existing.env.COZELOOP_API_TOKEN;
4535
+ }
4534
4536
  } else {
4535
4537
  existing.env.COZELOOP_API_TOKEN = patToken;
4538
+ delete existing.env.COZE_API_TOKEN;
4539
+ }
4540
+ if (process.env.COZELOOP_API_BASE_URL) {
4541
+ existing.env.COZELOOP_API_BASE_URL = process.env.COZELOOP_API_BASE_URL;
4542
+ delete existing.env.OTEL_ENDPOINT;
4543
+ } else if (process.env.OTEL_ENDPOINT) {
4544
+ existing.env.OTEL_ENDPOINT = process.env.OTEL_ENDPOINT;
4545
+ delete existing.env.COZELOOP_API_BASE_URL;
4536
4546
  }
4537
4547
  // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
4538
4548
  existing.env.x_tt_env = PPE_TT_ENV;
@@ -4875,6 +4885,103 @@ function httpsPost(url, body, extraHeaders) {
4875
4885
  });
4876
4886
  }
4877
4887
 
4888
+ function runCommand(input) {
4889
+ return new Promise((resolve) => {
4890
+ const { spawn } = require('child_process');
4891
+ const child = spawn(input.cmd, input.args || [], {
4892
+ env: input.env || process.env,
4893
+ stdio: ['pipe', 'pipe', 'pipe'],
4894
+ });
4895
+ let stdout = '';
4896
+ let stderr = '';
4897
+ let timedOut = false;
4898
+ const timer = setTimeout(() => {
4899
+ timedOut = true;
4900
+ child.kill('SIGKILL');
4901
+ }, input.timeoutMs || 30000);
4902
+ child.stdout.on('data', chunk => { stdout += chunk.toString(); });
4903
+ child.stderr.on('data', chunk => { stderr += chunk.toString(); });
4904
+ child.on('close', code => {
4905
+ clearTimeout(timer);
4906
+ resolve({ code: timedOut ? -1 : code, stdout, stderr, timedOut });
4907
+ });
4908
+ child.stdin.end(input.stdin || '');
4909
+ });
4910
+ }
4911
+
4912
+ async function verifyTraceReportViaSdk(token, workspaceId, pairCode, pythonCmd) {
4913
+ const pair = pairCode || crypto.randomBytes(6).toString('hex');
4914
+ const script = `
4915
+ import json
4916
+ import os
4917
+ import sys
4918
+
4919
+ events = []
4920
+
4921
+ def finish_event(info):
4922
+ if getattr(info, "is_event_fail", False):
4923
+ events.append(getattr(info, "detail_msg", "") or "trace export failed")
4924
+
4925
+ try:
4926
+ import cozeloop
4927
+ kwargs = {
4928
+ "workspace_id": os.environ["COZELOOP_WORKSPACE_ID"],
4929
+ "api_token": os.environ["COZELOOP_API_TOKEN"],
4930
+ "upload_timeout": 30,
4931
+ "trace_finish_event_processor": finish_event,
4932
+ }
4933
+ api_base_url = os.environ.get("COZELOOP_API_BASE_URL", "").strip()
4934
+ if api_base_url:
4935
+ kwargs["api_base_url"] = api_base_url
4936
+ client = cozeloop.new_client(**kwargs)
4937
+ with client.start_span(name="cozelab-onboard-selfcheck", span_type="main", start_new_trace=True) as span:
4938
+ span.set_tags({"pair_code": os.environ.get("COZELOOP_PAIR_CODE", ""), "source": "cozelab-onboard"})
4939
+ span.set_input("cozelab-onboard selfcheck")
4940
+ span.set_output("ok")
4941
+ client.flush()
4942
+ client.close()
4943
+ if events:
4944
+ print(json.dumps({"success": False, "body": "\\n".join(events)}, ensure_ascii=False))
4945
+ sys.exit(1)
4946
+ print(json.dumps({"success": True, "body": ""}, ensure_ascii=False))
4947
+ except Exception as e:
4948
+ print(json.dumps({"success": False, "body": str(e)}, ensure_ascii=False))
4949
+ sys.exit(1)
4950
+ `;
4951
+ const env = {
4952
+ ...process.env,
4953
+ COZELOOP_WORKSPACE_ID: workspaceId,
4954
+ COZELOOP_API_TOKEN: token,
4955
+ COZELOOP_PAIR_CODE: pair,
4956
+ x_tt_env: PPE_TT_ENV,
4957
+ x_use_ppe: PPE_USE_PPE,
4958
+ };
4959
+ const apiBase = getCozeloopApiBaseUrl(true);
4960
+ if (apiBase) env.COZELOOP_API_BASE_URL = apiBase;
4961
+ const result = await runCommand({
4962
+ cmd: pythonCmd || 'python3',
4963
+ args: ['-c', script],
4964
+ env,
4965
+ timeoutMs: 45000,
4966
+ });
4967
+ let parsed = null;
4968
+ const out = (result.stdout || '').trim().split(/\n/).filter(Boolean).pop();
4969
+ if (out) {
4970
+ try { parsed = JSON.parse(out); } catch { /* keep null */ }
4971
+ }
4972
+ const body = parsed?.body || result.stderr || result.stdout || (result.timedOut ? 'SDK selfcheck timed out' : '');
4973
+ const success = result.code === 0 && parsed?.success === true;
4974
+ if (success) {
4975
+ ok(`trace 上报成功 (pair_code=${pair})`);
4976
+ info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
4977
+ } else {
4978
+ warn(`trace 上报失败: SDK selfcheck exit ${result.code}`);
4979
+ const snippet = String(body || '').slice(0, 300);
4980
+ if (snippet) console.log(snippet);
4981
+ }
4982
+ return { success, status: result.code || 0, body, traceId: '', pairCode: pair };
4983
+ }
4984
+
4878
4985
  // 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
4879
4986
  // 只看 HTTP 状态码(2xx=通),不回查 trace 是否落库——回查由外部查询方完成。
4880
4987
  // pairCode 写进 span 的 pair_code attribute,供查询方按该字段过滤回查;缺省自动生成。
@@ -5389,7 +5496,9 @@ async function main() {
5389
5496
 
5390
5497
  // Step 5: Verify trace reporting end-to-end
5391
5498
  info('Step 5/5: 验证 trace 上报链路...');
5392
- const verifyResult = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(args.cloud));
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));
5393
5502
  if (verifyResult.success) {
5394
5503
  cloudResult.verify = 'ok';
5395
5504
  } else if (CLOUD_MODE) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -82,6 +82,7 @@ else:
82
82
  DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
83
83
  _COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
84
84
  _COZE_API = "https://api.coze.cn"
85
+ _OTEL_SUFFIX = "/v1/loop/opentelemetry"
85
86
  _REFRESH_THRESHOLD = 10 * 60 # refresh when < 10 minutes remain
86
87
  _DEFAULT_WORKSPACE_ID = "7644910356078837760" # hardcoded spaceID fallback
87
88
 
@@ -247,8 +248,32 @@ def _refresh_token(refresh_token: str) -> Optional[str]:
247
248
  debug_log(f"Token refresh failed: {e}")
248
249
  return None
249
250
 
251
+ def _normalize_api_base_url(url: str) -> str:
252
+ base = (url or "").strip().rstrip("/")
253
+ if base.endswith(_OTEL_SUFFIX + "/v1/traces"):
254
+ return base[:-len(_OTEL_SUFFIX + "/v1/traces")].rstrip("/")
255
+ if base.endswith("/api/v1/loop/opentelemetry/v1/traces"):
256
+ return base[:-len("/v1/loop/opentelemetry/v1/traces")].rstrip("/")
257
+ if base.endswith(_OTEL_SUFFIX):
258
+ return base[:-len(_OTEL_SUFFIX)].rstrip("/")
259
+ if base.endswith("/api/v1/loop/opentelemetry"):
260
+ return base[:-len("/v1/loop/opentelemetry")].rstrip("/")
261
+ if base.endswith("/api/v1"):
262
+ return base[:-len("/v1")].rstrip("/")
263
+ return base
264
+
265
+ def get_api_base_url() -> str:
266
+ return _normalize_api_base_url(
267
+ os.environ.get("COZELOOP_API_BASE_URL", "") or os.environ.get("OTEL_ENDPOINT", "")
268
+ )
269
+
250
270
  def get_fresh_token() -> Optional[str]:
251
271
  """Return a valid access token, refreshing if needed. Falls back to env var."""
272
+ api_base_url = get_api_base_url()
273
+ env_token = os.environ.get("COZELOOP_API_TOKEN")
274
+ 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
252
277
  creds = _load_credentials()
253
278
  if creds:
254
279
  expires_at_sec = creds.get("expires_at", 0) / 1000
@@ -262,8 +287,13 @@ def get_fresh_token() -> Optional[str]:
262
287
  if new_token:
263
288
  return new_token
264
289
  debug_log("Refresh failed, falling back to env var.")
265
- # Cloud sandbox: token lives in COZE_API_TOKEN (no credentials.json / refresh).
266
- return os.environ.get("COZELOOP_API_TOKEN") or os.environ.get("COZE_API_TOKEN")
290
+ if env_token:
291
+ return env_token
292
+ if api_base_url:
293
+ return env_coze_token
294
+ if env_coze_token:
295
+ debug_log("COZE_API_TOKEN present but ignored without COZELOOP_API_BASE_URL or OTEL_ENDPOINT")
296
+ return None
267
297
 
268
298
  # -------------------------------------------------------------------------
269
299
 
@@ -873,6 +903,9 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
873
903
  client_kwargs["workspace_id"] = workspace_id
874
904
  if token:
875
905
  client_kwargs["api_token"] = token
906
+ api_base_url = get_api_base_url()
907
+ if api_base_url:
908
+ client_kwargs["api_base_url"] = api_base_url
876
909
  client = cozeloop.new_client(**client_kwargs)
877
910
 
878
911
  try:
@@ -1448,5 +1481,3 @@ def main():
1448
1481
 
1449
1482
  if __name__ == "__main__":
1450
1483
  main()
1451
-
1452
-
@@ -230,6 +230,11 @@ 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
+ env_token = os.environ.get("COZELOOP_API_TOKEN")
235
+ 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
233
238
  creds = _load_credentials()
234
239
  if creds:
235
240
  remaining = creds.get("expires_at", 0) / 1000 - time.time()
@@ -239,12 +244,11 @@ def get_fresh_token():
239
244
  new_token = _refresh_token(creds["refresh_token"])
240
245
  if new_token:
241
246
  return new_token
242
- token = os.environ.get("COZELOOP_API_TOKEN")
243
- if token:
244
- return token
245
- if get_api_base_url():
246
- return os.environ.get("COZE_API_TOKEN")
247
- if os.environ.get("COZE_API_TOKEN"):
247
+ if env_token:
248
+ return env_token
249
+ if api_base_url:
250
+ return env_coze_token
251
+ if env_coze_token:
248
252
  hook_log("COZE_API_TOKEN present but ignored without COZELOOP_API_BASE_URL or OTEL_ENDPOINT")
249
253
  return None
250
254
  # -------------------------------------------------------------------------