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 +6 -5
- package/index.js +79 -35
- package/package.json +1 -1
- package/scripts/claude-code/cozeloop_hook.py +3 -3
- package/scripts/codex/cozeloop_hook.py +3 -3
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
|
|
74
|
-
`COZELOOP_API_TOKEN
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
5500
|
+
'云端 sandbox 应在进程环境中注入可用于 trace ingest 的 token。',
|
|
5441
5501
|
'未检测到该变量,无法配置 trace 上报。',
|
|
5442
5502
|
]);
|
|
5443
5503
|
}
|
|
5444
5504
|
cloudResult.token_source = tokenSource;
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
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
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
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
|
@@ -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", "")
|
|
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", "")
|
|
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
|
|