coze_lab 0.1.20 → 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
@@ -9,6 +9,7 @@ const PPE_TT_ENV = 'ppe_cozelab';
9
9
  const PPE_USE_PPE = '1';
10
10
  const COZE_API = 'https://api.coze.cn';
11
11
  const CREDS_PATH = require('path').join(require('os').homedir(), '.cozeloop', 'credentials.json');
12
+ const PACKAGE_VERSION = require('./package.json').version;
12
13
  // Refresh when less than 10 minutes remain
13
14
  const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
14
15
 
@@ -16,7 +17,7 @@ const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
16
17
  // 云端(--cloud)模式:在 stdout 输出一行机器可读结果 COZE_LAB_RESULT={...},
17
18
  // 供管理后台解析判定(inject/verify/logid/message),不依赖中文文案。
18
19
  let CLOUD_MODE = false;
19
- const cloudResult = { inject: 'skip', verify: 'skip', logid: '', message: '' };
20
+ const cloudResult = { version: PACKAGE_VERSION, inject: 'skip', verify: 'skip', logid: '', message: '' };
20
21
 
21
22
  // errorBox 在云端模式下抛此异常(而非 process.exit),由 main 外层统一收尾。
22
23
  class CloudAbort extends Error {}
@@ -4471,8 +4472,6 @@ function shellEnvLine(key, value) {
4471
4472
  // writeClaudeCodeHook 配置 Claude Code 的 hook。
4472
4473
  // configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
4473
4474
  // settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
4474
- // cloud=true 时不把明文 token 写进 settings.local.json —— 云端 hook 运行时直接读
4475
- // 环境变量 COZE_API_TOKEN(见 scripts/claude-code/cozeloop_hook.py 的 get_fresh_token)。
4476
4475
  function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cloud) {
4477
4476
  const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
4478
4477
  const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
@@ -4526,12 +4525,24 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
4526
4525
  const localSettings = mergeJson(localSettingsPath, (existing) => {
4527
4526
  if (!existing.env) existing.env = {};
4528
4527
  existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
4529
- // 云端:token 由 sandbox 注入到环境变量 COZE_API_TOKEN,hook 运行时直接读,
4530
- // 不在配置文件落明文 token。本地:写入 OAuth 拿到的 token。
4531
4528
  if (cloud) {
4532
- 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
+ }
4533
4536
  } else {
4534
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;
4535
4546
  }
4536
4547
  // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
4537
4548
  existing.env.x_tt_env = PPE_TT_ENV;
@@ -4874,6 +4885,103 @@ function httpsPost(url, body, extraHeaders) {
4874
4885
  });
4875
4886
  }
4876
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
+
4877
4985
  // 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
4878
4986
  // 只看 HTTP 状态码(2xx=通),不回查 trace 是否落库——回查由外部查询方完成。
4879
4987
  // pairCode 写进 span 的 pair_code attribute,供查询方按该字段过滤回查;缺省自动生成。
@@ -5201,7 +5309,7 @@ const NEXT_STEP = {
5201
5309
 
5202
5310
  async function main() {
5203
5311
  console.log('');
5204
- info('CozeLoop Onboard CLI starting...');
5312
+ info(`CozeLoop Onboard CLI starting... (coze_lab v${PACKAGE_VERSION})`);
5205
5313
  console.log('');
5206
5314
 
5207
5315
  const args = validateArgs(parseArgs());
@@ -5357,6 +5465,7 @@ async function main() {
5357
5465
 
5358
5466
  // Success summary(用实际写入路径,per-agent / 自定义 CODEX_HOME 时也准确)
5359
5467
  const summaryLines = [
5468
+ `Onboard: coze_lab v${PACKAGE_VERSION}`,
5360
5469
  `Agent: ${agent} v${version}`,
5361
5470
  ];
5362
5471
  if (args.agentId) summaryLines.push(`Agent ID: ${args.agentId}`);
@@ -5387,7 +5496,9 @@ async function main() {
5387
5496
 
5388
5497
  // Step 5: Verify trace reporting end-to-end
5389
5498
  info('Step 5/5: 验证 trace 上报链路...');
5390
- 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));
5391
5502
  if (verifyResult.success) {
5392
5503
  cloudResult.verify = 'ok';
5393
5504
  } else if (CLOUD_MODE) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.20",
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
  # -------------------------------------------------------------------------