coze_lab 0.1.51 → 0.1.53
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
|
@@ -20,12 +20,43 @@ class CloudAbort extends Error {}
|
|
|
20
20
|
// 兼容两种形态:JSON 里的 "logid":"xxx" / detail.logid,以及裸 logid=xxx。
|
|
21
21
|
function extractLogid(text) {
|
|
22
22
|
if (!text) return '';
|
|
23
|
-
let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9]+)"/);
|
|
23
|
+
let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
24
24
|
if (m) return m[1];
|
|
25
|
-
m = text.match(/
|
|
25
|
+
m = text.match(/"log_id"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
26
|
+
if (m) return m[1];
|
|
27
|
+
m = text.match(/"Logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
28
|
+
if (m) return m[1];
|
|
29
|
+
m = text.match(/logid[=:\s]+([a-zA-Z0-9._:-]+)/i);
|
|
26
30
|
return m ? m[1] : '';
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
function headerString(value) {
|
|
34
|
+
if (Array.isArray(value)) return value.find(Boolean) || '';
|
|
35
|
+
return value ? String(value) : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractLogidFromHeaders(headers) {
|
|
39
|
+
if (!headers) return '';
|
|
40
|
+
const candidates = [
|
|
41
|
+
'x-tt-logid',
|
|
42
|
+
'x-tt-log-id',
|
|
43
|
+
'x-log-id',
|
|
44
|
+
'x-logid',
|
|
45
|
+
'x-request-id',
|
|
46
|
+
'x-tt-trace-id',
|
|
47
|
+
'trace-id',
|
|
48
|
+
];
|
|
49
|
+
for (const key of candidates) {
|
|
50
|
+
const value = headerString(headers[key] || headers[key.toLowerCase()] || headers[key.toUpperCase()]);
|
|
51
|
+
if (value) return value;
|
|
52
|
+
}
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatLogid(logid) {
|
|
57
|
+
return logid ? `, logid=${logid}` : ', logid=';
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
// 输出结构化结果行(仅云端模式)。
|
|
30
61
|
function emitCloudResult() {
|
|
31
62
|
if (!CLOUD_MODE) return;
|
|
@@ -65,6 +96,7 @@ const C = {
|
|
|
65
96
|
};
|
|
66
97
|
|
|
67
98
|
function log(prefix, color, msg) {
|
|
99
|
+
if (process.env.COZELAB_ONBOARD_VERBOSE !== '1') return;
|
|
68
100
|
console.log(`${color}${prefix}${C.reset} ${msg}`);
|
|
69
101
|
}
|
|
70
102
|
const ok = (msg) => log('[✓]', C.green, msg);
|
|
@@ -92,6 +124,7 @@ function errorBox(lines) {
|
|
|
92
124
|
}
|
|
93
125
|
|
|
94
126
|
function warnBox(lines) {
|
|
127
|
+
if (process.env.COZELAB_ONBOARD_VERBOSE !== '1') return;
|
|
95
128
|
box(lines, C.yellow);
|
|
96
129
|
}
|
|
97
130
|
|
|
@@ -518,6 +551,48 @@ function isVersionAtLeast(version, minVersion) {
|
|
|
518
551
|
return true;
|
|
519
552
|
}
|
|
520
553
|
|
|
554
|
+
// Codex docs now use [features].hooks as the canonical key; codex_hooks is a
|
|
555
|
+
// deprecated alias. Keep the alias only for explicitly detected older CLIs.
|
|
556
|
+
const CODEX_CANONICAL_HOOKS_FEATURE_MIN_VERSION = '0.134.0';
|
|
557
|
+
|
|
558
|
+
function codexHooksFeatureKey(version) {
|
|
559
|
+
const hasKnownVersion = parseVersionNumbers(version).length > 0;
|
|
560
|
+
if (hasKnownVersion && !isVersionAtLeast(version, CODEX_CANONICAL_HOOKS_FEATURE_MIN_VERSION)) {
|
|
561
|
+
return 'codex_hooks';
|
|
562
|
+
}
|
|
563
|
+
return 'hooks';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function normalizeCodexHooksFeatureConfig(configToml, codexVersion) {
|
|
567
|
+
const featureKey = codexHooksFeatureKey(codexVersion);
|
|
568
|
+
const lines = String(configToml || '').replace(/\r\n/g, '\n').split('\n');
|
|
569
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
570
|
+
|
|
571
|
+
const featureHeaderRe = /^\s*\[features\]\s*(?:#.*)?$/;
|
|
572
|
+
const tableHeaderRe = /^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/;
|
|
573
|
+
const hookFeatureRe = /^\s*(hooks|codex_hooks)\s*=/;
|
|
574
|
+
const featureIdx = lines.findIndex((line) => featureHeaderRe.test(line));
|
|
575
|
+
|
|
576
|
+
if (featureIdx === -1) {
|
|
577
|
+
const out = [...lines];
|
|
578
|
+
if (out.length && out[out.length - 1].trim() !== '') out.push('');
|
|
579
|
+
out.push('[features]', `${featureKey} = true`);
|
|
580
|
+
return out.join('\n') + '\n';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let endIdx = featureIdx + 1;
|
|
584
|
+
while (endIdx < lines.length && !tableHeaderRe.test(lines[endIdx])) {
|
|
585
|
+
endIdx += 1;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const before = lines.slice(0, featureIdx);
|
|
589
|
+
const section = lines.slice(featureIdx, endIdx).filter((line, idx) => idx === 0 || !hookFeatureRe.test(line));
|
|
590
|
+
section.splice(1, 0, `${featureKey} = true`);
|
|
591
|
+
const after = lines.slice(endIdx);
|
|
592
|
+
|
|
593
|
+
return [...before, ...section, ...after].join('\n') + '\n';
|
|
594
|
+
}
|
|
595
|
+
|
|
521
596
|
// ─── 7. Hook writers ─────────────────────────────────────────────────────────
|
|
522
597
|
const fs = require('fs');
|
|
523
598
|
const path = require('path');
|
|
@@ -698,13 +773,14 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
|
|
|
698
773
|
// 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
|
|
699
774
|
// 本地 --agent-id 从 config.patToken 写入 token,并用 COZELOOP_TOKEN_SOURCE 标记来源。
|
|
700
775
|
// cloud=true 时写 COZELAB_ONBOARD_CLOUD,并带入 sandbox 注入的 trace token。
|
|
701
|
-
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSource) {
|
|
776
|
+
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSource, codexVersion) {
|
|
702
777
|
const home = codexHome || path.join(os.homedir(), '.codex');
|
|
703
778
|
const hooksDir = path.join(home, 'hooks');
|
|
704
779
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
705
780
|
const envFile = path.join(hooksDir, 'cozeloop.env');
|
|
706
781
|
const logFile = path.join(hooksDir, 'cozeloop.log');
|
|
707
782
|
const hooksJson = path.join(home, 'hooks.json');
|
|
783
|
+
const configToml = path.join(home, 'config.toml');
|
|
708
784
|
|
|
709
785
|
// 1. Write Python hook script
|
|
710
786
|
ensureDir(hooksDir);
|
|
@@ -779,9 +855,22 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
|
|
|
779
855
|
errorBox([`ERROR: Cannot write ${hooksJson}`, '', e.message]);
|
|
780
856
|
}
|
|
781
857
|
ok(`Hook registered in ${hooksJson}`);
|
|
858
|
+
|
|
859
|
+
// 4. Normalize Codex hook feature flag. hooks.json carries hook definitions;
|
|
860
|
+
// config.toml carries the on/off feature gate. Re-running onboard should
|
|
861
|
+
// clean the deprecated codex_hooks alias for modern Codex versions.
|
|
862
|
+
const featureKey = codexHooksFeatureKey(codexVersion);
|
|
863
|
+
try {
|
|
864
|
+
const existingConfig = fs.existsSync(configToml) ? fs.readFileSync(configToml, 'utf8') : '';
|
|
865
|
+
const nextConfig = normalizeCodexHooksFeatureConfig(existingConfig, codexVersion);
|
|
866
|
+
atomicWriteFileSync(configToml, nextConfig);
|
|
867
|
+
} catch (e) {
|
|
868
|
+
errorBox([`ERROR: Cannot update ${configToml}`, '', e.message]);
|
|
869
|
+
}
|
|
870
|
+
ok(`Codex hooks feature enabled via [features].${featureKey} in ${configToml}`);
|
|
782
871
|
warn('Codex hook trust: 首次启动 Codex 会提示 "Hooks need review"。在该提示按 t(Trust all and continue),或在会话内运行 /hooks 后按 t,即可一次性信任全部 hook 启用 trace 上报。');
|
|
783
872
|
|
|
784
|
-
return { hookScript, envFile, hooksJson, logFile };
|
|
873
|
+
return { hookScript, envFile, hooksJson, configToml, logFile };
|
|
785
874
|
}
|
|
786
875
|
|
|
787
876
|
function resolveCodexHome(args) {
|
|
@@ -1108,7 +1197,7 @@ function httpsPost(url, body, extraHeaders) {
|
|
|
1108
1197
|
(res) => {
|
|
1109
1198
|
let buf = '';
|
|
1110
1199
|
res.on('data', c => buf += c);
|
|
1111
|
-
res.on('end', () => resolve({ status: res.statusCode, body: buf }));
|
|
1200
|
+
res.on('end', () => resolve({ status: res.statusCode, body: buf, headers: res.headers || {}, logid: extractLogidFromHeaders(res.headers) || extractLogid(buf) }));
|
|
1112
1201
|
}
|
|
1113
1202
|
);
|
|
1114
1203
|
req.on('error', reject);
|
|
@@ -1166,7 +1255,7 @@ def extract_logid(text):
|
|
|
1166
1255
|
if idx >= 0:
|
|
1167
1256
|
out = []
|
|
1168
1257
|
for ch in text[idx + len(marker):]:
|
|
1169
|
-
if ch.isalnum():
|
|
1258
|
+
if ch.isalnum() or ch in "._:-":
|
|
1170
1259
|
out.append(ch)
|
|
1171
1260
|
else:
|
|
1172
1261
|
break
|
|
@@ -1255,15 +1344,13 @@ except Exception as e:
|
|
|
1255
1344
|
const body = parsed?.body || result.stderr || result.stdout || (result.timedOut ? 'SDK selfcheck timed out' : '');
|
|
1256
1345
|
const success = result.code === 0 && parsed?.success === true;
|
|
1257
1346
|
if (success) {
|
|
1258
|
-
ok(`trace 上报成功 (pair_code=${pair})`);
|
|
1347
|
+
ok(`trace 上报成功 (pair_code=${pair}${formatLogid(parsed?.logid || '')})`);
|
|
1259
1348
|
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
1260
1349
|
} else {
|
|
1261
1350
|
warn(`trace 上报失败: SDK selfcheck exit ${result.code}`);
|
|
1262
1351
|
info(`trace selfcheck api_base_url=${apiBase || COZE_API}, token_source=${tokenSource || 'unknown'}`);
|
|
1263
|
-
const snippet = String(body || '').slice(0, 300);
|
|
1264
|
-
if (snippet) console.log(snippet);
|
|
1265
1352
|
}
|
|
1266
|
-
return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || '' };
|
|
1353
|
+
return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || extractLogid(body) || '' };
|
|
1267
1354
|
}
|
|
1268
1355
|
|
|
1269
1356
|
async function verifyCloudTraceReport(token, workspaceId, pairCode, tokenSource) {
|
|
@@ -1330,14 +1417,12 @@ async function verifyCloudTraceReport(token, workspaceId, pairCode, tokenSource)
|
|
|
1330
1417
|
);
|
|
1331
1418
|
const success = res.status >= 200 && res.status < 300;
|
|
1332
1419
|
if (success) {
|
|
1333
|
-
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
|
|
1420
|
+
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair}${formatLogid(res.logid || '')})`);
|
|
1334
1421
|
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
1335
1422
|
} else {
|
|
1336
|
-
warn(`trace 上报失败: HTTP ${res.status}`);
|
|
1337
|
-
const snippet = (res.body || '').slice(0, 300);
|
|
1338
|
-
if (snippet) console.log(snippet);
|
|
1423
|
+
warn(`trace 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')}`);
|
|
1339
1424
|
}
|
|
1340
|
-
return { success, status: res.status, body: res.body || '', traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: extractLogid(res.body || '') };
|
|
1425
|
+
return { success, status: res.status, body: res.body || '', traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: res.logid || extractLogid(res.body || '') };
|
|
1341
1426
|
} catch (e) {
|
|
1342
1427
|
warn(`trace 上报失败: ${e.message}`);
|
|
1343
1428
|
return { success: false, status: 0, body: e.message, traceId, pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: extractLogid(e.message) };
|
|
@@ -1396,14 +1481,12 @@ async function verifyTraceReport(token, workspaceId, pairCode, tracesUrl) {
|
|
|
1396
1481
|
|
|
1397
1482
|
const success = res.status >= 200 && res.status < 300;
|
|
1398
1483
|
if (success) {
|
|
1399
|
-
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
|
|
1484
|
+
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair}${formatLogid(res.logid || '')})`);
|
|
1400
1485
|
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
1401
1486
|
} else {
|
|
1402
|
-
warn(`trace 上报失败: HTTP ${res.status}`);
|
|
1403
|
-
const snippet = (res.body || '').slice(0, 300);
|
|
1404
|
-
if (snippet) console.log(snippet);
|
|
1487
|
+
warn(`trace 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')}`);
|
|
1405
1488
|
}
|
|
1406
|
-
return { success, status: res.status, body: res.body, traceId, pairCode: pair };
|
|
1489
|
+
return { success, status: res.status, body: res.body, traceId, pairCode: pair, logid: res.logid || extractLogid(res.body || '') };
|
|
1407
1490
|
}
|
|
1408
1491
|
|
|
1409
1492
|
// ── OpenClaw 专属上报链路校验 ──────────────────────────────────────────────
|
|
@@ -1500,11 +1583,9 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
|
1500
1583
|
const tokenPrefix = authHeader.replace(/^Bearer\s+/i, '').slice(0, 12);
|
|
1501
1584
|
|
|
1502
1585
|
if (success) {
|
|
1503
|
-
ok(`openclaw 插件实际 token 上报正常 (token=${tokenPrefix}..., HTTP ${res.status})`);
|
|
1586
|
+
ok(`openclaw 插件实际 token 上报正常 (token=${tokenPrefix}..., HTTP ${res.status}${formatLogid(res.logid || '')})`);
|
|
1504
1587
|
} else {
|
|
1505
|
-
warn(`openclaw 插件实际 token 上报失败: HTTP ${res.status} (token=${tokenPrefix}...)`);
|
|
1506
|
-
const snippet = (res.body || '').slice(0, 300);
|
|
1507
|
-
if (snippet) console.log(snippet);
|
|
1588
|
+
warn(`openclaw 插件实际 token 上报失败: HTTP ${res.status}${formatLogid(res.logid || '')} (token=${tokenPrefix}...)`);
|
|
1508
1589
|
// 4100/401 = 该 token 已失效。指出根因与修复方式。
|
|
1509
1590
|
if (res.status === 401 || /\b4100\b/.test(res.body || '')) {
|
|
1510
1591
|
info('插件配置的 token 已失效。运行时上报会 401,span 会丢失。');
|
|
@@ -1512,7 +1593,7 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
|
1512
1593
|
}
|
|
1513
1594
|
}
|
|
1514
1595
|
|
|
1515
|
-
return { success, status: res.status, body: res.body || '' };
|
|
1596
|
+
return { success, status: res.status, body: res.body || '', logid: res.logid || extractLogid(res.body || '') };
|
|
1516
1597
|
}
|
|
1517
1598
|
|
|
1518
1599
|
function httpsGet(url, headers) {
|
|
@@ -1545,9 +1626,7 @@ function openClawNextStep(written) {
|
|
|
1545
1626
|
}
|
|
1546
1627
|
|
|
1547
1628
|
async function main() {
|
|
1548
|
-
console.log('');
|
|
1549
1629
|
info(`CozeLoop Onboard CLI starting... (coze_lab v${PACKAGE_VERSION})`);
|
|
1550
|
-
console.log('');
|
|
1551
1630
|
|
|
1552
1631
|
const args = validateArgs(parseArgs());
|
|
1553
1632
|
|
|
@@ -1576,7 +1655,6 @@ async function main() {
|
|
|
1576
1655
|
'请确认 ~/.coze/agents/<id>/config.json 包含 patToken 后重试。',
|
|
1577
1656
|
]);
|
|
1578
1657
|
}
|
|
1579
|
-
console.log('');
|
|
1580
1658
|
const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
|
|
1581
1659
|
// 若本机装了 openclaw 插件,额外校验插件【实际配置的静态 token】——通用 verify 用刚刷新的
|
|
1582
1660
|
// token 测不到它。任一失败则整体判失败(exit 1)。
|
|
@@ -1586,14 +1664,28 @@ async function main() {
|
|
|
1586
1664
|
try {
|
|
1587
1665
|
const cfg = JSON.parse(fs.readFileSync(ocConfigPath, 'utf8'));
|
|
1588
1666
|
if (cfg?.plugins?.entries?.['openclaw-cozeloop-trace']?.config?.authorization) {
|
|
1589
|
-
console.log('');
|
|
1590
1667
|
info('检测到 openclaw cozeloop-trace 插件,校验其实际 token...');
|
|
1591
1668
|
const ocRes = await verifyOpenClawTraceLink(args.cloud, args.pairCode);
|
|
1592
1669
|
ocOk = ocRes.success;
|
|
1593
1670
|
}
|
|
1594
1671
|
} catch { /* 读不了就跳过 openclaw 校验 */ }
|
|
1595
1672
|
}
|
|
1596
|
-
|
|
1673
|
+
if (result.success && ocOk) {
|
|
1674
|
+
successBox([
|
|
1675
|
+
`Verify: coze_lab v${PACKAGE_VERSION}`,
|
|
1676
|
+
`Trace: OK (pair_code=${result.pairCode})`,
|
|
1677
|
+
]);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
errorBox([
|
|
1681
|
+
'ERROR: trace 上报自检失败',
|
|
1682
|
+
'',
|
|
1683
|
+
`HTTP ${result.status}`,
|
|
1684
|
+
`logid=${result.logid || extractLogid(result.body) || ''}`,
|
|
1685
|
+
(result.body || '').slice(0, 300),
|
|
1686
|
+
'',
|
|
1687
|
+
'请确认 CozeLoop Token 具备 Trace ingest 权限,再重试。',
|
|
1688
|
+
]);
|
|
1597
1689
|
}
|
|
1598
1690
|
|
|
1599
1691
|
const { agent } = args;
|
|
@@ -1607,7 +1699,6 @@ async function main() {
|
|
|
1607
1699
|
}
|
|
1608
1700
|
if (args.agentId) {
|
|
1609
1701
|
info(`目标 agent: ${args.agentId} (framework=${agent}, workspace=${agentWorkspace || 'N/A'}, deploy=${args.cloud ? 'cloud' : 'local'}, reason=${args.deployReason || 'n/a'})`);
|
|
1610
|
-
console.log('');
|
|
1611
1702
|
}
|
|
1612
1703
|
|
|
1613
1704
|
// Step 1: Resolve trace token.
|
|
@@ -1652,12 +1743,9 @@ async function main() {
|
|
|
1652
1743
|
]);
|
|
1653
1744
|
}
|
|
1654
1745
|
}
|
|
1655
|
-
console.log('');
|
|
1656
|
-
|
|
1657
1746
|
// Step 2: Detect agent binary
|
|
1658
1747
|
info('Step 2/5: Detecting agent...');
|
|
1659
1748
|
const version = detectAgent(agent, args.cloud);
|
|
1660
|
-
console.log('');
|
|
1661
1749
|
|
|
1662
1750
|
// Step 3: Environment checks
|
|
1663
1751
|
info('Step 3/5: Checking environment...');
|
|
@@ -1667,7 +1755,6 @@ async function main() {
|
|
|
1667
1755
|
checkCozeloopSdk(pythonCmd, { cloud: args.cloud });
|
|
1668
1756
|
}
|
|
1669
1757
|
checkVersionWhitelist(agent, version);
|
|
1670
|
-
console.log('');
|
|
1671
1758
|
|
|
1672
1759
|
// Step 4: Write hook configuration
|
|
1673
1760
|
info('Step 4/5: Writing hook configuration...');
|
|
@@ -1692,14 +1779,13 @@ async function main() {
|
|
|
1692
1779
|
info(`已创建云端 Codex 配置目录: ${codexHome}`);
|
|
1693
1780
|
}
|
|
1694
1781
|
if (codexHome) info(`Codex 配置目录: ${codexHome}`);
|
|
1695
|
-
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud, tokenSource);
|
|
1782
|
+
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud, tokenSource, version);
|
|
1696
1783
|
} else {
|
|
1697
1784
|
// openclaw:云端用 traceAgentIds allowlist 做 per-agent 放行。
|
|
1698
1785
|
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force, tokenSource) || {};
|
|
1699
1786
|
}
|
|
1700
1787
|
// 走到这里说明 detectAgent / 环境检查 / 写 hook 配置全部成功 → 注入成功。
|
|
1701
1788
|
cloudResult.inject = 'ok';
|
|
1702
|
-
console.log('');
|
|
1703
1789
|
|
|
1704
1790
|
// Success summary(用实际写入路径,per-agent / 自定义 CODEX_HOME 时也准确)
|
|
1705
1791
|
const summaryLines = [
|
|
@@ -1714,6 +1800,7 @@ async function main() {
|
|
|
1714
1800
|
} else if (agent === 'codex') {
|
|
1715
1801
|
summaryLines.push(`Hook script: ${written.hookScript || '~/.codex/hooks/cozeloop_hook.py'}`);
|
|
1716
1802
|
summaryLines.push(`Config: ${written.hooksJson || '~/.codex/hooks.json'}`);
|
|
1803
|
+
if (written.configToml) summaryLines.push(`Feature: ${written.configToml}`);
|
|
1717
1804
|
summaryLines.push(`Credentials: ${written.envFile || '~/.codex/hooks/cozeloop.env'}`);
|
|
1718
1805
|
if (written.logFile) summaryLines.push(`Hook log: ${written.logFile}`);
|
|
1719
1806
|
} else {
|
|
@@ -1724,13 +1811,10 @@ async function main() {
|
|
|
1724
1811
|
|
|
1725
1812
|
// Enterprise policy note for Claude Code
|
|
1726
1813
|
if (agent === 'claude-code') {
|
|
1727
|
-
console.log('');
|
|
1728
1814
|
info('注意:企业托管模式下若 allowManagedHooksOnly=true,需管理员在策略中放行本地 hook。');
|
|
1729
1815
|
}
|
|
1730
1816
|
// Trace permission reminder for all agents
|
|
1731
|
-
console.log('');
|
|
1732
1817
|
info('确保 CozeLoop Token 具备 Trace ingest 权限,否则 hook 触发但上报会失败。');
|
|
1733
|
-
console.log('');
|
|
1734
1818
|
|
|
1735
1819
|
// Step 5: Verify trace reporting end-to-end
|
|
1736
1820
|
info('Step 5/5: 验证 trace 上报链路...');
|
|
@@ -1745,13 +1829,13 @@ async function main() {
|
|
|
1745
1829
|
: await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
|
|
1746
1830
|
if (verifyResult.success) {
|
|
1747
1831
|
cloudResult.verify = 'ok';
|
|
1832
|
+
cloudResult.logid = verifyResult.logid || extractLogid(verifyResult.body) || cloudResult.logid;
|
|
1748
1833
|
} else if (CLOUD_MODE) {
|
|
1749
1834
|
// 云端:注入已成功,验证失败不阻断(放行),记录结果供后台弹 warning。
|
|
1750
1835
|
cloudResult.verify = 'fail';
|
|
1751
1836
|
cloudResult.logid = verifyResult.logid || extractLogid(verifyResult.body) || cloudResult.logid;
|
|
1752
1837
|
cloudResult.message = `trace 上报自检失败 HTTP ${verifyResult.status}: ${(verifyResult.body || '').slice(0, 200)}`;
|
|
1753
1838
|
warn('trace 上报自检失败,但 hook 配置已写入(云端放行)。');
|
|
1754
|
-
console.log('');
|
|
1755
1839
|
emitCloudResult();
|
|
1756
1840
|
successBox(summaryLines);
|
|
1757
1841
|
return;
|
|
@@ -1760,13 +1844,13 @@ async function main() {
|
|
|
1760
1844
|
'ERROR: trace 上报自检失败',
|
|
1761
1845
|
'',
|
|
1762
1846
|
`HTTP ${verifyResult.status}`,
|
|
1847
|
+
`logid=${verifyResult.logid || extractLogid(verifyResult.body) || ''}`,
|
|
1763
1848
|
(verifyResult.body || '').slice(0, 300),
|
|
1764
1849
|
'',
|
|
1765
1850
|
'hook 配置已写入,但上报链路未打通。',
|
|
1766
1851
|
'请确认 CozeLoop Token 具备 Trace ingest 权限,再重试。',
|
|
1767
1852
|
]); // errorBox 内部 process.exit(1)
|
|
1768
1853
|
}
|
|
1769
|
-
console.log('');
|
|
1770
1854
|
|
|
1771
1855
|
emitCloudResult();
|
|
1772
1856
|
successBox(summaryLines);
|
|
@@ -1800,5 +1884,7 @@ module.exports = {
|
|
|
1800
1884
|
atomicWriteFileSync,
|
|
1801
1885
|
VERSION_WHITELIST,
|
|
1802
1886
|
VERSION_SUPPORT,
|
|
1887
|
+
codexHooksFeatureKey,
|
|
1888
|
+
normalizeCodexHooksFeatureConfig,
|
|
1803
1889
|
isVersionAtLeast,
|
|
1804
1890
|
};
|
package/package.json
CHANGED
|
@@ -47,6 +47,48 @@ function fileLog(logFile, message) {
|
|
|
47
47
|
/* best-effort, never throw from logging */
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
function headerString(value) {
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
return value.find(Boolean) || "";
|
|
53
|
+
return value ? String(value) : "";
|
|
54
|
+
}
|
|
55
|
+
function extractLogidFromHeaders(headers) {
|
|
56
|
+
if (!headers)
|
|
57
|
+
return "";
|
|
58
|
+
const candidates = [
|
|
59
|
+
"x-tt-logid",
|
|
60
|
+
"x-tt-log-id",
|
|
61
|
+
"x-log-id",
|
|
62
|
+
"x-logid",
|
|
63
|
+
"x-request-id",
|
|
64
|
+
"x-tt-trace-id",
|
|
65
|
+
"trace-id",
|
|
66
|
+
];
|
|
67
|
+
for (const key of candidates) {
|
|
68
|
+
const value = headerString(headers[key] || headers[key.toLowerCase()] || headers[key.toUpperCase()]);
|
|
69
|
+
if (value)
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
function extractLogid(text) {
|
|
75
|
+
if (!text)
|
|
76
|
+
return "";
|
|
77
|
+
let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
78
|
+
if (m)
|
|
79
|
+
return m[1];
|
|
80
|
+
m = text.match(/"log_id"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
81
|
+
if (m)
|
|
82
|
+
return m[1];
|
|
83
|
+
m = text.match(/"Logid"\s*:\s*"([a-zA-Z0-9._:-]+)"/);
|
|
84
|
+
if (m)
|
|
85
|
+
return m[1];
|
|
86
|
+
m = text.match(/logid[=:\s]+([a-zA-Z0-9._:-]+)/i);
|
|
87
|
+
return m ? m[1] : "";
|
|
88
|
+
}
|
|
89
|
+
function formatLogid(logid) {
|
|
90
|
+
return logid ? ` logid=${logid}` : " logid=";
|
|
91
|
+
}
|
|
50
92
|
|
|
51
93
|
function normalizeApiBaseUrl(endpoint) {
|
|
52
94
|
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
@@ -215,7 +257,12 @@ function postJson(url, body, headers) {
|
|
|
215
257
|
}, (res) => {
|
|
216
258
|
let buf = "";
|
|
217
259
|
res.on("data", c => buf += c);
|
|
218
|
-
res.on("end", () => resolve({
|
|
260
|
+
res.on("end", () => resolve({
|
|
261
|
+
status: res.statusCode || 0,
|
|
262
|
+
body: buf,
|
|
263
|
+
headers: res.headers || {},
|
|
264
|
+
logid: extractLogidFromHeaders(res.headers) || extractLogid(buf),
|
|
265
|
+
}));
|
|
219
266
|
});
|
|
220
267
|
req.on("error", reject);
|
|
221
268
|
req.setTimeout(15000, () => req.destroy(new Error("CozeLoop ingest export timed out")));
|
|
@@ -305,8 +352,8 @@ class CozeloopIngestExporter {
|
|
|
305
352
|
const res = await postJson(this.url, body, this.headers);
|
|
306
353
|
if (res.status < 200 || res.status >= 300) {
|
|
307
354
|
const snippet = String(res.body || "").slice(0, 300);
|
|
308
|
-
fileLog(this.logFile, `[ingest] HTTP ${res.status} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
|
|
309
|
-
throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
|
|
355
|
+
fileLog(this.logFile, `[ingest] HTTP ${res.status}${formatLogid(res.logid)} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
|
|
356
|
+
throw new Error(`HTTP ${res.status}${formatLogid(res.logid)}${snippet ? `: ${snippet}` : ""}`);
|
|
310
357
|
}
|
|
311
358
|
if (res.body) {
|
|
312
359
|
try {
|
|
@@ -322,7 +369,7 @@ class CozeloopIngestExporter {
|
|
|
322
369
|
}
|
|
323
370
|
}
|
|
324
371
|
}
|
|
325
|
-
fileLog(this.logFile, `[ingest] OK HTTP ${res.status} spans=${body.spans.length}${retry ? " retry=1" : ""}`);
|
|
372
|
+
fileLog(this.logFile, `[ingest] OK HTTP ${res.status}${formatLogid(res.logid)} spans=${body.spans.length}${retry ? " retry=1" : ""}`);
|
|
326
373
|
}
|
|
327
374
|
async forceFlush() {
|
|
328
375
|
// Wait for every in-flight POST to settle. allSettled (not all) so one
|
|
@@ -196,6 +196,54 @@ function formatAssistantOutput(content, stopReason) {
|
|
|
196
196
|
],
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
+
function usageInt(usage, keys) {
|
|
200
|
+
if (!usage || typeof usage !== "object")
|
|
201
|
+
return 0;
|
|
202
|
+
for (const key of keys) {
|
|
203
|
+
const value = usage[key];
|
|
204
|
+
if (value === undefined || value === null || value === "")
|
|
205
|
+
continue;
|
|
206
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
207
|
+
if (Number.isFinite(n) && n > 0)
|
|
208
|
+
return Math.trunc(n);
|
|
209
|
+
}
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
function readTokenUsage(...usages) {
|
|
213
|
+
const inputKeys = [
|
|
214
|
+
"input",
|
|
215
|
+
"input_tokens",
|
|
216
|
+
"inputTokens",
|
|
217
|
+
"prompt_tokens",
|
|
218
|
+
"promptTokens",
|
|
219
|
+
"prompt",
|
|
220
|
+
];
|
|
221
|
+
const outputKeys = [
|
|
222
|
+
"output",
|
|
223
|
+
"output_tokens",
|
|
224
|
+
"outputTokens",
|
|
225
|
+
"completion_tokens",
|
|
226
|
+
"completionTokens",
|
|
227
|
+
"completion",
|
|
228
|
+
];
|
|
229
|
+
const totalKeys = ["total", "total_tokens", "totalTokens"];
|
|
230
|
+
let input = 0;
|
|
231
|
+
let output = 0;
|
|
232
|
+
let total = 0;
|
|
233
|
+
for (const usage of usages) {
|
|
234
|
+
if (!usage || typeof usage !== "object")
|
|
235
|
+
continue;
|
|
236
|
+
if (input === 0)
|
|
237
|
+
input = usageInt(usage, inputKeys);
|
|
238
|
+
if (output === 0)
|
|
239
|
+
output = usageInt(usage, outputKeys);
|
|
240
|
+
if (total === 0)
|
|
241
|
+
total = usageInt(usage, totalKeys);
|
|
242
|
+
if (input > 0 && output > 0 && total > 0)
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
return { input, output, total };
|
|
246
|
+
}
|
|
199
247
|
function convertAssistantContentForMessages(content) {
|
|
200
248
|
if (!Array.isArray(content))
|
|
201
249
|
return [{ type: "text", text: String(content ?? "") }];
|
|
@@ -901,11 +949,15 @@ const cozeloopTracePlugin = {
|
|
|
901
949
|
const model = entry.model || ctx.lastModelId || "unknown";
|
|
902
950
|
const spanStartTime = prevWrittenAt;
|
|
903
951
|
const spanEndTime = entryWrittenAt;
|
|
952
|
+
const tokenUsage = readTokenUsage(entry.usage);
|
|
904
953
|
const modelSpan = createSpan(ctx, channelId, `${provider}/${model}`, "model", spanStartTime, spanEndTime, {
|
|
905
954
|
"gen_ai.provider.name": provider,
|
|
906
955
|
"gen_ai.request.model": model,
|
|
907
|
-
"gen_ai.usage.input_tokens":
|
|
908
|
-
"gen_ai.usage.output_tokens":
|
|
956
|
+
"gen_ai.usage.input_tokens": tokenUsage.input,
|
|
957
|
+
"gen_ai.usage.output_tokens": tokenUsage.output,
|
|
958
|
+
"gen_ai.usage.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
|
|
959
|
+
"input_tokens": tokenUsage.input,
|
|
960
|
+
"output_tokens": tokenUsage.output,
|
|
909
961
|
"react_round": reactRound,
|
|
910
962
|
}, { messages: reactMessages.map((msg) => safeClone(msg)) }, formatAssistantOutput(entry.content, entry.stopReason));
|
|
911
963
|
await exporter.export(modelSpan);
|
|
@@ -1269,13 +1321,15 @@ const cozeloopTracePlugin = {
|
|
|
1269
1321
|
}
|
|
1270
1322
|
}
|
|
1271
1323
|
const lastAssistantUsage = event.lastAssistant?.usage;
|
|
1272
|
-
const
|
|
1273
|
-
const outputTokens = event.usage?.output ?? lastAssistantUsage?.output ?? 0;
|
|
1324
|
+
const tokenUsage = readTokenUsage(event.usage, lastAssistantUsage);
|
|
1274
1325
|
const spanAttributes = {
|
|
1275
1326
|
"gen_ai.provider.name": event.provider,
|
|
1276
1327
|
"gen_ai.request.model": event.model,
|
|
1277
|
-
"gen_ai.usage.input_tokens":
|
|
1278
|
-
"gen_ai.usage.output_tokens":
|
|
1328
|
+
"gen_ai.usage.input_tokens": tokenUsage.input,
|
|
1329
|
+
"gen_ai.usage.output_tokens": tokenUsage.output,
|
|
1330
|
+
"gen_ai.usage.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
|
|
1331
|
+
"input_tokens": tokenUsage.input,
|
|
1332
|
+
"output_tokens": tokenUsage.output,
|
|
1279
1333
|
};
|
|
1280
1334
|
const finalOutput = formatAssistantOutput(event.assistantTexts?.map((t) => ({ type: "text", text: t })) ?? [], "stop");
|
|
1281
1335
|
const span = createSpan(ctx, channelId, `${event.provider}/${event.model}`, "model", startTime, now, spanAttributes, llmInput, finalOutput);
|
|
@@ -1595,11 +1649,12 @@ const cozeloopTracePlugin = {
|
|
|
1595
1649
|
const active = resolveActiveAgentContext(hookCtx, rawChannelId);
|
|
1596
1650
|
const ctx = active?.ctx || getOrCreateContext(rawChannelId, undefined, "agent_end").ctx;
|
|
1597
1651
|
const channelId = active?.channelId || rawChannelId;
|
|
1652
|
+
const tokenUsage = readTokenUsage(event.usage);
|
|
1598
1653
|
finalizeTrace(ctx, channelId, {
|
|
1599
1654
|
"agent.duration_ms": event.durationMs || 0,
|
|
1600
1655
|
"agent.message_count": event.messageCount || 0,
|
|
1601
1656
|
"agent.tool_call_count": event.toolCallCount || 0,
|
|
1602
|
-
"agent.total_tokens":
|
|
1657
|
+
"agent.total_tokens": tokenUsage.total || tokenUsage.input + tokenUsage.output,
|
|
1603
1658
|
}, { usage: event.usage, cost: event.cost });
|
|
1604
1659
|
});
|
|
1605
1660
|
}
|