coze_lab 0.1.17 → 0.1.19

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
@@ -4464,6 +4464,10 @@ function mergeJson(filepath, mergeFn) {
4464
4464
  return mergeFn(existing);
4465
4465
  }
4466
4466
 
4467
+ function shellEnvLine(key, value) {
4468
+ return `${key}='${String(value).replace(/'/g, `'\\''`)}'`;
4469
+ }
4470
+
4467
4471
  // writeClaudeCodeHook 配置 Claude Code 的 hook。
4468
4472
  // configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
4469
4473
  // settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
@@ -4565,19 +4569,29 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4565
4569
  writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
4566
4570
 
4567
4571
  // 2. Write env file with chmod 600
4568
- // 云端(cloud):不落明文 token,hook 运行时从环境变量 COZE_API_TOKEN 读取。
4569
4572
  const envLines = [
4570
- `COZELOOP_WORKSPACE_ID=${workspaceId}`,
4573
+ shellEnvLine('COZELOOP_WORKSPACE_ID', workspaceId),
4571
4574
  ];
4572
- if (!cloud) {
4573
- envLines.push(`COZELOOP_API_TOKEN=${token}`);
4575
+ if (cloud) {
4576
+ if (process.env.COZELOOP_API_TOKEN) {
4577
+ envLines.push(shellEnvLine('COZELOOP_API_TOKEN', process.env.COZELOOP_API_TOKEN));
4578
+ } else if (process.env.COZE_API_TOKEN) {
4579
+ envLines.push(shellEnvLine('COZE_API_TOKEN', process.env.COZE_API_TOKEN));
4580
+ }
4581
+ } else {
4582
+ envLines.push(shellEnvLine('COZELOOP_API_TOKEN', token));
4574
4583
  }
4575
- envLines.push(`CODEX_HOME=${home}`);
4576
- envLines.push(`COZELOOP_HOOK_LOG=${logFile}`);
4584
+ envLines.push(shellEnvLine('CODEX_HOME', home));
4585
+ envLines.push(shellEnvLine('COZELOOP_HOOK_LOG', logFile));
4577
4586
  envLines.push('TRACE_TO_COZELOOP=true');
4587
+ if (process.env.COZELOOP_API_BASE_URL) {
4588
+ envLines.push(shellEnvLine('COZELOOP_API_BASE_URL', process.env.COZELOOP_API_BASE_URL));
4589
+ } else if (process.env.OTEL_ENDPOINT) {
4590
+ envLines.push(shellEnvLine('OTEL_ENDPOINT', process.env.OTEL_ENDPOINT));
4591
+ }
4578
4592
  // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
4579
- envLines.push(`x_tt_env=${PPE_TT_ENV}`);
4580
- envLines.push(`x_use_ppe=${PPE_USE_PPE}`);
4593
+ envLines.push(shellEnvLine('x_tt_env', PPE_TT_ENV));
4594
+ envLines.push(shellEnvLine('x_use_ppe', PPE_USE_PPE));
4581
4595
  const envContent = envLines.join('\n') + '\n';
4582
4596
  try {
4583
4597
  fs.writeFileSync(envFile, envContent, { mode: 0o600 });
@@ -4661,6 +4675,11 @@ function getOpenClawEndpoint(cloud) {
4661
4675
  : 'https://api.coze.cn/v1/loop/opentelemetry';
4662
4676
  }
4663
4677
 
4678
+ function getCloudCozeloopBaseUrl() {
4679
+ const endpoint = process.env.COZELOOP_API_BASE_URL || process.env.OTEL_ENDPOINT || '';
4680
+ return endpoint.replace(/\/v1\/loop\/opentelemetry\/?$/, '').replace(/\/+$/, '') || undefined;
4681
+ }
4682
+
4664
4683
  function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud) {
4665
4684
  if (!existing.plugins) existing.plugins = {};
4666
4685
  if (!existing.plugins.allow) existing.plugins.allow = [];
@@ -4815,7 +4834,7 @@ function httpsPost(url, body, extraHeaders) {
4815
4834
  const data = JSON.stringify(body);
4816
4835
  const u = new URL(url);
4817
4836
  const req = https.request(
4818
- { hostname: u.hostname, path: u.pathname + u.search, method: 'POST',
4837
+ { hostname: u.hostname, port: u.port || undefined, path: u.pathname + u.search, method: 'POST',
4819
4838
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data),
4820
4839
  'x-tt-env': PPE_TT_ENV, 'x-use-ppe': PPE_USE_PPE,
4821
4840
  ...(extraHeaders || {}) } },
@@ -5225,7 +5244,7 @@ async function main() {
5225
5244
  info('验证 trace 上报链路...');
5226
5245
  const token = await getValidToken(); // 无凭证会自动走登录/刷新
5227
5246
  console.log('');
5228
- const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, process.env.COZELOOP_API_BASE_URL || undefined);
5247
+ const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getCloudCozeloopBaseUrl());
5229
5248
  process.exit(result.success ? 0 : 1);
5230
5249
  }
5231
5250
 
@@ -5347,7 +5366,7 @@ async function main() {
5347
5366
 
5348
5367
  // Step 5: Verify trace reporting end-to-end
5349
5368
  info('Step 5/5: 验证 trace 上报链路...');
5350
- const verifyResult = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, args.cloud ? process.env.COZELOOP_API_BASE_URL : undefined);
5369
+ const verifyResult = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, args.cloud ? getCloudCozeloopBaseUrl() : undefined);
5351
5370
  if (verifyResult.success) {
5352
5371
  cloudResult.verify = 'ok';
5353
5372
  } else if (CLOUD_MODE) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -46,6 +46,7 @@ _COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
46
46
  _COZE_API = "https://api.coze.cn"
47
47
  _REFRESH_THRESHOLD = 10 * 60
48
48
  _DEFAULT_WORKSPACE_ID = "7644910356078837760" # hardcoded spaceID fallback
49
+ _OTEL_SUFFIX = "/v1/loop/opentelemetry"
49
50
 
50
51
 
51
52
  # --- coze-context parsing -------------------------------------------------
@@ -98,6 +99,22 @@ def coze_context_tags(text: str) -> Dict[str, str]:
98
99
  return {f"coze_{k}": v for k, v in parse_coze_context(text).items()}
99
100
 
100
101
 
102
+ def turn_coze_context(turn: Dict[str, Any]) -> Dict[str, str]:
103
+ """Extract coze-context from a grouped turn with fallbacks for Codex rollout shapes."""
104
+ texts = [turn.get("user_message_text", "")]
105
+ for msg in turn.get("input_messages", []):
106
+ if isinstance(msg, dict):
107
+ texts.append(str(msg.get("content", "")))
108
+ user_payload = turn.get("user_message")
109
+ if isinstance(user_payload, dict):
110
+ texts.append(extract_message_content_text(user_payload))
111
+ for text in texts:
112
+ ctx = parse_coze_context(text)
113
+ if ctx:
114
+ return ctx
115
+ return {}
116
+
117
+
101
118
  # --- trace upload failure / logid capture ---------------------------------
102
119
  def _extract_logid(msg: str) -> str:
103
120
  """Pull the server logid out of an SDK error message, if present.
@@ -190,6 +207,20 @@ def _refresh_token(refresh_tok: str):
190
207
  pass
191
208
  return None
192
209
 
210
+
211
+ def _normalize_api_base_url(url: str) -> str:
212
+ base = (url or "").strip().rstrip("/")
213
+ if base.endswith(_OTEL_SUFFIX):
214
+ return base[:-len(_OTEL_SUFFIX)].rstrip("/")
215
+ return base
216
+
217
+
218
+ def get_api_base_url() -> str:
219
+ return _normalize_api_base_url(
220
+ os.environ.get("COZELOOP_API_BASE_URL", "") or os.environ.get("OTEL_ENDPOINT", "")
221
+ )
222
+
223
+
193
224
  def get_fresh_token():
194
225
  creds = _load_credentials()
195
226
  if creds:
@@ -200,8 +231,14 @@ def get_fresh_token():
200
231
  new_token = _refresh_token(creds["refresh_token"])
201
232
  if new_token:
202
233
  return new_token
203
- # Cloud sandbox: token lives in COZE_API_TOKEN (no credentials.json / refresh).
204
- return os.environ.get("COZELOOP_API_TOKEN") or os.environ.get("COZE_API_TOKEN")
234
+ token = os.environ.get("COZELOOP_API_TOKEN")
235
+ if token:
236
+ return token
237
+ if get_api_base_url():
238
+ return os.environ.get("COZE_API_TOKEN")
239
+ if os.environ.get("COZE_API_TOKEN"):
240
+ hook_log("COZE_API_TOKEN present but ignored without COZELOOP_API_BASE_URL or OTEL_ENDPOINT")
241
+ return None
205
242
  # -------------------------------------------------------------------------
206
243
 
207
244
  # --- SDK Import ---
@@ -516,6 +553,8 @@ def is_real_user_message(payload: Dict[str, Any]) -> bool:
516
553
  if item.get("type") != "input_text":
517
554
  continue
518
555
  text = item.get("text", "")
556
+ if parse_coze_context(text):
557
+ return True
519
558
  if text.startswith("<environment_context>"):
520
559
  continue
521
560
  if text.startswith("<permissions instructions>"):
@@ -534,9 +573,10 @@ def extract_user_text(payload: Dict[str, Any]) -> str:
534
573
  for item in payload.get("content", []):
535
574
  if isinstance(item, dict) and item.get("type") == "input_text":
536
575
  text = item.get("text", "")
537
- if (not text.startswith("<environment_context>") and
576
+ if (parse_coze_context(text) or
577
+ (not text.startswith("<environment_context>") and
538
578
  not text.startswith("<permissions instructions>") and
539
- not text.startswith("<turn_aborted>")):
579
+ not text.startswith("<turn_aborted>"))):
540
580
  parts.append(text)
541
581
  return "\n".join(parts)
542
582
 
@@ -810,7 +850,12 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
810
850
  hook_log(f"token resolved prefix={token[:12]}...")
811
851
  print(f"[CozeLoop] Token 获取成功 ({token[:12]}...)", file=sys.stderr)
812
852
  else:
813
- hook_log("token missing")
853
+ hook_log(
854
+ "token missing "
855
+ f"has_cozeloop_token={bool(os.environ.get('COZELOOP_API_TOKEN'))} "
856
+ f"has_coze_token={bool(os.environ.get('COZE_API_TOKEN'))} "
857
+ f"api_base_url={bool(get_api_base_url())}"
858
+ )
814
859
  print("[CozeLoop] 警告: 未找到有效 Token,上报可能失败", file=sys.stderr)
815
860
  creds = _load_credentials()
816
861
  workspace_id = (creds or {}).get("workspace_id") or os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
@@ -821,6 +866,10 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
821
866
  "upload_timeout": 120,
822
867
  "trace_finish_event_processor": _make_finish_event_processor(),
823
868
  }
869
+ api_base_url = get_api_base_url()
870
+ if api_base_url:
871
+ client_kwargs["api_base_url"] = api_base_url
872
+ hook_log(f"api_base_url={api_base_url}")
824
873
  if workspace_id:
825
874
  client_kwargs["workspace_id"] = workspace_id
826
875
  if token:
@@ -859,7 +908,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
859
908
  # Inject coze-context kv (last occurrence across turns wins).
860
909
  coze_tags = {}
861
910
  for turn in turns:
862
- t = coze_context_tags(turn.get("user_message_text", ""))
911
+ t = {f"coze_{k}": v for k, v in turn_coze_context(turn).items()}
863
912
  if t:
864
913
  coze_tags = t
865
914
  if coze_tags:
@@ -1276,7 +1325,7 @@ def main():
1276
1325
  # Send turns to CozeLoop — only if at least one turn carries coze-context.
1277
1326
  if turns:
1278
1327
  has_coze_ctx = any(
1279
- parse_coze_context(t.get("user_message_text", ""))
1328
+ turn_coze_context(t)
1280
1329
  for t in turns
1281
1330
  )
1282
1331
  if not has_coze_ctx:
@@ -22,7 +22,11 @@ function safeClone(value) {
22
22
  // --- coze-context parsing ---------------------------------------------------
23
23
  // User input may embed a <coze-context>...</coze-context> block with key:value
24
24
  // lines (agent_id, session_id, message_id, account_id, ...). Parse the LAST
25
- // block and inject the pairs (prefixed "coze.") into the root span attributes.
25
+ // block and inject the pairs (prefixed "coze_") into the root span attributes.
26
+ // NOTE: prefix MUST be "coze_" (underscore) to match the cozelab backend trace
27
+ // query field names (coze_message_id / coze_agent_id) and stay consistent with
28
+ // the claude-code / codex hooks. Do NOT use "coze." (dot) — backend Eq filter
29
+ // matches tag keys exactly and would never hit dotted keys.
26
30
  const COZE_CTX_OPEN = "<coze-context>";
27
31
  const COZE_CTX_CLOSE = "</coze-context>";
28
32
  function cozeInputToText(input) {
@@ -67,7 +71,7 @@ function parseCozeContext(input) {
67
71
  const key = line.slice(0, sep).trim();
68
72
  const value = line.slice(sep + 1).trim();
69
73
  if (key)
70
- out["coze." + key] = value;
74
+ out["coze_" + key] = value;
71
75
  }
72
76
  return out;
73
77
  }