coze_lab 0.1.33 → 0.1.35
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 +101 -39
- package/package.json +1 -1
- package/scripts/claude-code/cozeloop_hook.py +47 -11
- package/scripts/codex/cozeloop_hook.py +16 -10
- package/scripts/openclaw/dist/cozeloop-exporter.js +51 -14
- package/scripts/openclaw/dist/index.js +1 -0
- package/scripts/openclaw/openclaw.plugin.json +6 -1
- package/scripts/openclaw/package.json +1 -1
package/index.js
CHANGED
|
@@ -5172,6 +5172,7 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
5172
5172
|
const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
|
|
5173
5173
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
5174
5174
|
const baseDir = configBaseDir || process.cwd();
|
|
5175
|
+
const logFile = path.join(baseDir, '.claude', 'hooks', 'cozeloop.log');
|
|
5175
5176
|
// per-agent 时 settings.json 写进 agent 的 .claude;全局时仍写 ~/.claude/settings.json。
|
|
5176
5177
|
const claudeDir = configBaseDir
|
|
5177
5178
|
? path.join(baseDir, '.claude')
|
|
@@ -5218,9 +5219,11 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
5218
5219
|
|
|
5219
5220
|
// 3. Write credentials into <baseDir>/.claude/settings.local.json
|
|
5220
5221
|
ensureDir(path.join(baseDir, '.claude'));
|
|
5222
|
+
ensureDir(path.dirname(logFile));
|
|
5221
5223
|
const localSettings = mergeJson(localSettingsPath, (existing) => {
|
|
5222
5224
|
if (!existing.env) existing.env = {};
|
|
5223
5225
|
existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
|
|
5226
|
+
existing.env.COZELOOP_HOOK_LOG = logFile;
|
|
5224
5227
|
if (cloud) {
|
|
5225
5228
|
const loopToken = readEnv('COZELOOP_API_TOKEN');
|
|
5226
5229
|
const cozeToken = readEnv('COZE_API_TOKEN');
|
|
@@ -5259,7 +5262,7 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
5259
5262
|
ok(`Credentials written to ${localSettingsPath}`);
|
|
5260
5263
|
|
|
5261
5264
|
|
|
5262
|
-
return { hookScript, settingsPath, localSettingsPath };
|
|
5265
|
+
return { hookScript, settingsPath, localSettingsPath, logFile };
|
|
5263
5266
|
}
|
|
5264
5267
|
|
|
5265
5268
|
// writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
|
|
@@ -5386,33 +5389,32 @@ function normalizeTraceAgentIds(ids) {
|
|
|
5386
5389
|
}
|
|
5387
5390
|
|
|
5388
5391
|
function getCloudCozeloopApiBaseUrl() {
|
|
5389
|
-
const raw = process.env.COZELOOP_API_BASE_URL ||
|
|
5392
|
+
const raw = process.env.COZELOOP_API_BASE_URL || '';
|
|
5390
5393
|
return normalizeCozeloopApiBaseUrl(raw);
|
|
5391
5394
|
}
|
|
5392
5395
|
|
|
5393
5396
|
function normalizeCozeloopApiBaseUrl(raw) {
|
|
5394
|
-
const base = raw.trim().replace(/\/+$/, '');
|
|
5397
|
+
const base = (raw || '').trim().replace(/\/+$/, '');
|
|
5395
5398
|
if (!base) return '';
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
return base.slice(0, -'/v1'.length).replace(/\/+$/, '');
|
|
5399
|
+
// 剥除已知 loop 路径后缀,还原出纯 API base。
|
|
5400
|
+
// 关键:cozeloop ingest 正确端点是 <host>/v1/loop/traces/ingest,【不带 /api 前缀】。
|
|
5401
|
+
// 含 /api 的变体必须连 /api 一起剥掉 —— 否则残留 /api 会拼出 /api/v1/loop/traces/ingest,
|
|
5402
|
+
// 命中不处理 ingest 协议的网关入口 → 后端 400 "unknown event type"。
|
|
5403
|
+
// 顺序约定:更长 / 带 /api 的变体在前,最短的裸 /v1 在最后,避免 endsWith 误匹配短后缀。
|
|
5404
|
+
const suffixes = [
|
|
5405
|
+
'/api/v1/loop/traces/ingest',
|
|
5406
|
+
'/v1/loop/traces/ingest',
|
|
5407
|
+
'/api/v1/loop/opentelemetry/v1/traces',
|
|
5408
|
+
'/v1/loop/opentelemetry/v1/traces',
|
|
5409
|
+
'/api/v1/loop/opentelemetry',
|
|
5410
|
+
'/v1/loop/opentelemetry',
|
|
5411
|
+
'/api/v1',
|
|
5412
|
+
'/v1',
|
|
5413
|
+
];
|
|
5414
|
+
for (const suffix of suffixes) {
|
|
5415
|
+
if (base.endsWith(suffix)) {
|
|
5416
|
+
return base.slice(0, -suffix.length).replace(/\/+$/, '');
|
|
5417
|
+
}
|
|
5416
5418
|
}
|
|
5417
5419
|
return base;
|
|
5418
5420
|
}
|
|
@@ -5445,7 +5447,41 @@ function getCozeloopIngestUrl(cloud) {
|
|
|
5445
5447
|
return getCozeloopIngestUrlFromBase(getCozeloopApiBaseUrl(cloud));
|
|
5446
5448
|
}
|
|
5447
5449
|
|
|
5448
|
-
function
|
|
5450
|
+
function buildOpenClawSelfcheckTags(pcfg, workspaceId, pair) {
|
|
5451
|
+
const tags = {
|
|
5452
|
+
pair_code: pair,
|
|
5453
|
+
source: 'cozelab-onboard-openclaw',
|
|
5454
|
+
};
|
|
5455
|
+
const allowlist = normalizeTraceAgentIds(pcfg?.traceAgentIds);
|
|
5456
|
+
if (allowlist[0]) {
|
|
5457
|
+
tags.coze_agent_id = allowlist[0];
|
|
5458
|
+
}
|
|
5459
|
+
const sessionId = readEnv('COZELOOP_SELF_CHECK_SESSION_ID')
|
|
5460
|
+
|| readEnv('COZE_SESSION_ID')
|
|
5461
|
+
|| readEnv('COZELOOP_SESSION_ID');
|
|
5462
|
+
if (sessionId) {
|
|
5463
|
+
tags.coze_session_id = sessionId;
|
|
5464
|
+
}
|
|
5465
|
+
const messageId = readEnv('COZELOOP_SELF_CHECK_MESSAGE_ID')
|
|
5466
|
+
|| readEnv('COZE_MESSAGE_ID')
|
|
5467
|
+
|| readEnv('COZELOOP_MESSAGE_ID')
|
|
5468
|
+
|| `cozelab-onboard-${pair}`;
|
|
5469
|
+
tags.coze_message_id = messageId;
|
|
5470
|
+
const groupId = readEnv('COZE_GROUP_ID') || readEnv('COZELOOP_GROUP_ID');
|
|
5471
|
+
if (groupId) {
|
|
5472
|
+
tags.coze_group_id = groupId;
|
|
5473
|
+
}
|
|
5474
|
+
const accountId = readEnv('COZE_ACCOUNT_ID') || readEnv('COZELOOP_ACCOUNT_ID');
|
|
5475
|
+
if (accountId) {
|
|
5476
|
+
tags.coze_account_id = accountId;
|
|
5477
|
+
}
|
|
5478
|
+
if (workspaceId) {
|
|
5479
|
+
tags.coze_workspace_id = String(workspaceId);
|
|
5480
|
+
}
|
|
5481
|
+
return tags;
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile) {
|
|
5449
5485
|
if (!existing.plugins) existing.plugins = {};
|
|
5450
5486
|
if (!existing.plugins.allow) existing.plugins.allow = [];
|
|
5451
5487
|
if (!existing.plugins.entries) existing.plugins.entries = {};
|
|
@@ -5469,6 +5505,9 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud)
|
|
|
5469
5505
|
pcfg.endpoint = getCozeloopApiBaseUrl(cloud);
|
|
5470
5506
|
pcfg.workspaceId = workspaceId;
|
|
5471
5507
|
pcfg.debug = true;
|
|
5508
|
+
if (logFile) {
|
|
5509
|
+
pcfg.logFile = logFile;
|
|
5510
|
+
}
|
|
5472
5511
|
pcfg.disableLocalCredentials = !!cloud;
|
|
5473
5512
|
// 插件代码版本:参与幂等比对,升级插件(bump scripts/openclaw/package.json version)后
|
|
5474
5513
|
// 强制触发重写+重装+重启,避免云端 pluginDir 滞留旧插件 dist。
|
|
@@ -5487,7 +5526,7 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud)
|
|
|
5487
5526
|
return existing;
|
|
5488
5527
|
}
|
|
5489
5528
|
|
|
5490
|
-
function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud) {
|
|
5529
|
+
function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile) {
|
|
5491
5530
|
if (!fs.existsSync(pluginDir)) return false;
|
|
5492
5531
|
let existing;
|
|
5493
5532
|
try {
|
|
@@ -5501,14 +5540,16 @@ function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, ag
|
|
|
5501
5540
|
workspaceId,
|
|
5502
5541
|
agentId,
|
|
5503
5542
|
cloud,
|
|
5543
|
+
logFile,
|
|
5504
5544
|
);
|
|
5505
5545
|
return JSON.stringify(existing) === JSON.stringify(desired);
|
|
5506
5546
|
}
|
|
5507
5547
|
|
|
5508
|
-
function writeOpenClawHook(token, workspaceId, agentId, cloud) {
|
|
5548
|
+
function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
5509
5549
|
const home = resolveHomeDir(cloud);
|
|
5510
5550
|
const configPath = path.join(home, '.openclaw', 'openclaw.json');
|
|
5511
5551
|
const pluginDir = path.join(home, '.cozeloop', 'openclaw-plugin');
|
|
5552
|
+
const logFile = path.join(pluginDir, 'cozeloop.log');
|
|
5512
5553
|
|
|
5513
5554
|
if (!fs.existsSync(configPath)) {
|
|
5514
5555
|
errorBox([
|
|
@@ -5519,10 +5560,13 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud) {
|
|
|
5519
5560
|
]);
|
|
5520
5561
|
}
|
|
5521
5562
|
|
|
5522
|
-
if (isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud)) {
|
|
5563
|
+
if (!force && isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile)) {
|
|
5523
5564
|
ok(`OpenClaw plugin already configured in ${configPath}`);
|
|
5524
5565
|
info('OpenClaw gateway restart skipped (configuration unchanged).');
|
|
5525
|
-
return { configPath, pluginDir, unchanged: true, gatewayRestarted: false, gatewayRestartSkipped: true };
|
|
5566
|
+
return { configPath, pluginDir, logFile, unchanged: true, gatewayRestarted: false, gatewayRestartSkipped: true };
|
|
5567
|
+
}
|
|
5568
|
+
if (force) {
|
|
5569
|
+
info('OpenClaw force mode enabled; rewriting plugin files and reinstalling.');
|
|
5526
5570
|
}
|
|
5527
5571
|
|
|
5528
5572
|
// 1. Write plugin files to ~/.cozeloop/openclaw-plugin/
|
|
@@ -5575,7 +5619,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud) {
|
|
|
5575
5619
|
|
|
5576
5620
|
// 4. Update openclaw.json with token and workspace
|
|
5577
5621
|
const config = mergeJson(configPath, (existing) => {
|
|
5578
|
-
return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud);
|
|
5622
|
+
return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile);
|
|
5579
5623
|
});
|
|
5580
5624
|
|
|
5581
5625
|
try {
|
|
@@ -5591,10 +5635,10 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud) {
|
|
|
5591
5635
|
info('Restarting OpenClaw gateway to apply hook changes...');
|
|
5592
5636
|
execSync('openclaw gateway restart', { stdio: 'pipe' });
|
|
5593
5637
|
ok('OpenClaw gateway restarted');
|
|
5594
|
-
return { configPath, pluginDir, gatewayRestarted: true };
|
|
5638
|
+
return { configPath, pluginDir, logFile, gatewayRestarted: true };
|
|
5595
5639
|
} catch (e) {
|
|
5596
5640
|
warn(`gateway restart 失败,请手动执行: openclaw gateway restart(${e.message})`);
|
|
5597
|
-
return { configPath, pluginDir, gatewayRestarted: false, gatewayRestartError: e.message };
|
|
5641
|
+
return { configPath, pluginDir, logFile, gatewayRestarted: false, gatewayRestartError: e.message };
|
|
5598
5642
|
}
|
|
5599
5643
|
}
|
|
5600
5644
|
|
|
@@ -5749,6 +5793,8 @@ except Exception as e:
|
|
|
5749
5793
|
x_use_ppe: PPE_USE_PPE,
|
|
5750
5794
|
};
|
|
5751
5795
|
if (apiBase) env.COZELOOP_API_BASE_URL = apiBase;
|
|
5796
|
+
// 打出 SDK 实际使用的 ingest base,便于核对云端注入的 api_base_url 是否正确(无 /api 残留)。
|
|
5797
|
+
info(`trace 上报 URL: ${getCozeloopIngestUrlFromBase(apiBase || COZE_API)} (api_base_url=${apiBase || '(default)'})`);
|
|
5752
5798
|
const result = await runCommand({
|
|
5753
5799
|
cmd: pythonCmd || 'python3',
|
|
5754
5800
|
args: ['-c', script],
|
|
@@ -5812,6 +5858,7 @@ async function verifyTraceReport(token, workspaceId, pairCode, tracesUrl) {
|
|
|
5812
5858
|
|
|
5813
5859
|
let res;
|
|
5814
5860
|
const url = tracesUrl || getOtelTracesUrl(false);
|
|
5861
|
+
info(`trace 上报 URL: ${url}`);
|
|
5815
5862
|
try {
|
|
5816
5863
|
res = await httpsPost(
|
|
5817
5864
|
url,
|
|
@@ -5867,6 +5914,8 @@ async function verifyOpenClawTraceLink(cloud) {
|
|
|
5867
5914
|
// 1) 用插件【实际配置的】token 打一条最小 CozeLoop ingest trace ——这才是运行时真实用的那个 token。
|
|
5868
5915
|
const authHeader = pcfg.authorization; // 形如 "Bearer czu_xxx"
|
|
5869
5916
|
const tracesUrl = getCozeloopIngestUrlFromBase(normalizeCozeloopApiBaseUrl(pcfg.endpoint || getCozeloopApiBaseUrl(cloud)));
|
|
5917
|
+
// 打出实际 ingest URL —— 云端只能看 stdout,借此立即发现 URL 是否仍残留 /api 前缀(400 根因)。
|
|
5918
|
+
info(`openclaw ingest URL: ${tracesUrl} (plugin endpoint=${pcfg.endpoint || '(default)'})`);
|
|
5870
5919
|
const workspaceId = pcfg.workspaceId || WORKSPACE_ID;
|
|
5871
5920
|
const traceId = crypto.randomBytes(16).toString('hex');
|
|
5872
5921
|
const spanId = crypto.randomBytes(8).toString('hex');
|
|
@@ -5880,7 +5929,7 @@ async function verifyOpenClawTraceLink(cloud) {
|
|
|
5880
5929
|
parent_id: '0',
|
|
5881
5930
|
trace_id: traceId,
|
|
5882
5931
|
duration_micros: 1,
|
|
5883
|
-
service_name: '
|
|
5932
|
+
service_name: '',
|
|
5884
5933
|
workspace_id: String(workspaceId),
|
|
5885
5934
|
span_name: 'cozelab-onboard-openclaw-selfcheck',
|
|
5886
5935
|
span_type: 'main',
|
|
@@ -5889,14 +5938,16 @@ async function verifyOpenClawTraceLink(cloud) {
|
|
|
5889
5938
|
output: 'ok',
|
|
5890
5939
|
object_storage: '',
|
|
5891
5940
|
system_tags_string: {
|
|
5892
|
-
runtime: JSON.stringify({
|
|
5941
|
+
runtime: JSON.stringify({
|
|
5942
|
+
language: 'nodejs',
|
|
5943
|
+
library: 'openclaw',
|
|
5944
|
+
scene: process.env.COZELOOP_SCENE || 'custom',
|
|
5945
|
+
loop_sdk_version: `coze_lab@${PACKAGE_VERSION}`,
|
|
5946
|
+
}),
|
|
5893
5947
|
},
|
|
5894
5948
|
system_tags_long: {},
|
|
5895
5949
|
system_tags_double: {},
|
|
5896
|
-
tags_string:
|
|
5897
|
-
pair_code: pair,
|
|
5898
|
-
source: 'cozelab-onboard-openclaw',
|
|
5899
|
-
},
|
|
5950
|
+
tags_string: buildOpenClawSelfcheckTags(pcfg, workspaceId, pair),
|
|
5900
5951
|
tags_long: {},
|
|
5901
5952
|
tags_double: {},
|
|
5902
5953
|
tags_bool: {},
|
|
@@ -5907,7 +5958,18 @@ async function verifyOpenClawTraceLink(cloud) {
|
|
|
5907
5958
|
res = await httpsPost(
|
|
5908
5959
|
tracesUrl,
|
|
5909
5960
|
ingestBody,
|
|
5910
|
-
{
|
|
5961
|
+
{
|
|
5962
|
+
Authorization: authHeader,
|
|
5963
|
+
'User-Agent': `coze_lab/${PACKAGE_VERSION} node/${process.versions.node}`,
|
|
5964
|
+
'X-Coze-Client-User-Agent': JSON.stringify({
|
|
5965
|
+
version: PACKAGE_VERSION,
|
|
5966
|
+
lang: 'nodejs',
|
|
5967
|
+
lang_version: process.versions.node,
|
|
5968
|
+
os_name: process.platform,
|
|
5969
|
+
scene: 'cozeloop',
|
|
5970
|
+
source: 'openapi',
|
|
5971
|
+
}),
|
|
5972
|
+
},
|
|
5911
5973
|
);
|
|
5912
5974
|
} catch (e) {
|
|
5913
5975
|
warn(`openclaw 插件 token 上报探测失败: ${e.message}`);
|
|
@@ -6424,7 +6486,7 @@ async function main() {
|
|
|
6424
6486
|
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
|
|
6425
6487
|
} else {
|
|
6426
6488
|
// openclaw:云端用 traceAgentIds allowlist 做 per-agent 放行。
|
|
6427
|
-
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud) || {};
|
|
6489
|
+
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force) || {};
|
|
6428
6490
|
}
|
|
6429
6491
|
// 走到这里说明 detectAgent / 环境检查 / 写 hook 配置全部成功 → 注入成功。
|
|
6430
6492
|
cloudResult.inject = 'ok';
|
package/package.json
CHANGED
|
@@ -212,16 +212,41 @@ def _make_finish_event_processor(upload_events: Optional[List[str]] = None):
|
|
|
212
212
|
logid = _extract_logid(detail)
|
|
213
213
|
if logid:
|
|
214
214
|
print(f"[CozeLoop] 上报失败 logid={logid} (可用 bytedcli log get-logid-log {logid} 排查)", file=sys.stderr)
|
|
215
|
+
hook_log(f"upload FAILED logid={logid} detail={detail[:300]}")
|
|
215
216
|
else:
|
|
216
217
|
print(f"[CozeLoop] 上报失败: {detail[:300]}", file=sys.stderr)
|
|
218
|
+
hook_log(f"upload FAILED detail={detail[:300]}")
|
|
217
219
|
except Exception:
|
|
218
220
|
pass
|
|
219
221
|
return _processor
|
|
220
222
|
|
|
221
223
|
|
|
222
224
|
|
|
225
|
+
def _log_file_path() -> str:
|
|
226
|
+
return os.environ.get("COZELOOP_HOOK_LOG", "").strip()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def hook_log(message: str):
|
|
230
|
+
"""Append one diagnostic line to the hook log, if configured.
|
|
231
|
+
|
|
232
|
+
onboard writes COZELOOP_HOOK_LOG into settings.local.json (env), so this
|
|
233
|
+
captures runtime upload diagnostics even when stderr is not visible.
|
|
234
|
+
"""
|
|
235
|
+
log_path = _log_file_path()
|
|
236
|
+
if not log_path:
|
|
237
|
+
return
|
|
238
|
+
try:
|
|
239
|
+
p = Path(log_path).expanduser()
|
|
240
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
with p.open("a", encoding="utf-8") as f:
|
|
242
|
+
f.write(f"{datetime.now().isoformat()} {message}\n")
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
223
247
|
def debug_log(message: str):
|
|
224
|
-
"""Print debug message if debug mode is enabled."""
|
|
248
|
+
"""Print debug message if debug mode is enabled; always append to hook log."""
|
|
249
|
+
hook_log(f"DEBUG {message}")
|
|
225
250
|
if DEBUG:
|
|
226
251
|
print(f"[COZELOOP_HOOK_DEBUG] {datetime.now().isoformat()} - {message}", file=sys.stderr)
|
|
227
252
|
|
|
@@ -281,16 +306,22 @@ def _refresh_token(refresh_token: str) -> Optional[str]:
|
|
|
281
306
|
|
|
282
307
|
def _normalize_api_base_url(url: str) -> str:
|
|
283
308
|
base = (url or "").strip().rstrip("/")
|
|
284
|
-
if base
|
|
285
|
-
return base
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
309
|
+
if not base:
|
|
310
|
+
return base
|
|
311
|
+
# 剥除已知 loop 路径后缀,还原纯 API base。
|
|
312
|
+
# 关键:含 /api 的变体必须连 /api 整体剥掉,否则残留 /api 会拼出 /api/v1/loop/... → 后端 400。
|
|
313
|
+
# 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免误匹配短后缀。
|
|
314
|
+
suffixes = (
|
|
315
|
+
"/api/v1/loop/opentelemetry/v1/traces",
|
|
316
|
+
_OTEL_SUFFIX + "/v1/traces",
|
|
317
|
+
"/api/v1/loop/opentelemetry",
|
|
318
|
+
_OTEL_SUFFIX,
|
|
319
|
+
"/api/v1",
|
|
320
|
+
"/v1",
|
|
321
|
+
)
|
|
322
|
+
for suffix in suffixes:
|
|
323
|
+
if base.endswith(suffix):
|
|
324
|
+
return base[:-len(suffix)].rstrip("/")
|
|
294
325
|
return base
|
|
295
326
|
|
|
296
327
|
def get_api_base_url() -> str:
|
|
@@ -970,6 +1001,11 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
970
1001
|
api_base_url = get_api_base_url()
|
|
971
1002
|
if api_base_url:
|
|
972
1003
|
client_kwargs["api_base_url"] = api_base_url
|
|
1004
|
+
hook_log(
|
|
1005
|
+
f"init client session={session_id} workspace={workspace_id} "
|
|
1006
|
+
f"token={(token[:12] + '...') if token else 'none'} "
|
|
1007
|
+
f"api_base_url={api_base_url or '(default)'}"
|
|
1008
|
+
)
|
|
973
1009
|
client = cozeloop.new_client(**client_kwargs)
|
|
974
1010
|
|
|
975
1011
|
try:
|
|
@@ -218,16 +218,22 @@ def _refresh_token(refresh_tok: str):
|
|
|
218
218
|
|
|
219
219
|
def _normalize_api_base_url(url: str) -> str:
|
|
220
220
|
base = (url or "").strip().rstrip("/")
|
|
221
|
-
if base
|
|
222
|
-
return base
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
if not base:
|
|
222
|
+
return base
|
|
223
|
+
# 剥除已知 loop 路径后缀,还原纯 API base。
|
|
224
|
+
# 关键:含 /api 的变体必须连 /api 整体剥掉,否则残留 /api 会拼出 /api/v1/loop/... → 后端 400。
|
|
225
|
+
# 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免误匹配短后缀。
|
|
226
|
+
suffixes = (
|
|
227
|
+
"/api/v1/loop/opentelemetry/v1/traces",
|
|
228
|
+
_OTEL_SUFFIX + "/v1/traces",
|
|
229
|
+
"/api/v1/loop/opentelemetry",
|
|
230
|
+
_OTEL_SUFFIX,
|
|
231
|
+
"/api/v1",
|
|
232
|
+
"/v1",
|
|
233
|
+
)
|
|
234
|
+
for suffix in suffixes:
|
|
235
|
+
if base.endswith(suffix):
|
|
236
|
+
return base[:-len(suffix)].rstrip("/")
|
|
231
237
|
return base
|
|
232
238
|
|
|
233
239
|
|
|
@@ -5,13 +5,21 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_INSTANCE_ID } from "@opentelemetry/sema
|
|
|
5
5
|
import { hostname } from "os";
|
|
6
6
|
import { basename, join } from "path";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import http from "http";
|
|
11
11
|
import https from "https";
|
|
12
12
|
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
14
14
|
const { version: PLUGIN_VERSION } = require("../package.json");
|
|
15
|
+
const CLIENT_USER_AGENT = {
|
|
16
|
+
version: PLUGIN_VERSION,
|
|
17
|
+
lang: "nodejs",
|
|
18
|
+
lang_version: process.versions.node,
|
|
19
|
+
os_name: process.platform,
|
|
20
|
+
scene: "cozeloop",
|
|
21
|
+
source: "openapi",
|
|
22
|
+
};
|
|
15
23
|
|
|
16
24
|
// ── Token refresh helpers ─────────────────────────────────────────────────
|
|
17
25
|
const _CLIENT_ID = "46371084383473718052118955183420.app.coze";
|
|
@@ -82,24 +90,41 @@ const EXPORT_SUCCESS = 0;
|
|
|
82
90
|
const EXPORT_FAILED = 1;
|
|
83
91
|
const INGEST_TRACE_PATH = "/v1/loop/traces/ingest";
|
|
84
92
|
|
|
93
|
+
// 把一行诊断写入 onboard 配置的 logFile(pluginConfig.logFile)。云端 gateway 日志常拿不到
|
|
94
|
+
// (无 DBUS / 无 stdout 落盘),这个文件是排查上报失败的唯一依据。无 logFile 或写失败都静默。
|
|
95
|
+
function fileLog(logFile, message) {
|
|
96
|
+
if (!logFile)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
appendFileSync(logFile, `${new Date().toISOString()} ${message}\n`);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* best-effort, never throw from logging */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
function normalizeApiBaseUrl(endpoint) {
|
|
86
107
|
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
87
108
|
if (!base)
|
|
88
109
|
return _COZE_API;
|
|
110
|
+
// 剥除已知 loop 路径后缀,还原纯 API base。
|
|
111
|
+
// 关键:cozeloop ingest 正确端点 <host>/v1/loop/traces/ingest,【不带 /api 前缀】。
|
|
112
|
+
// 含 /api 的变体必须连 /api 整体剥掉 —— 否则残留 /api → /api/v1/loop/traces/ingest → 后端 400。
|
|
113
|
+
// 顺序:更长 / 带 /api 的在前,裸 /v1 最后,避免 endsWith 误匹配短后缀。
|
|
89
114
|
const suffixes = [
|
|
90
|
-
"/v1/loop/traces/ingest",
|
|
91
115
|
"/api/v1/loop/traces/ingest",
|
|
92
|
-
"/v1/loop/
|
|
116
|
+
"/v1/loop/traces/ingest",
|
|
93
117
|
"/api/v1/loop/opentelemetry/v1/traces",
|
|
94
|
-
"/v1/loop/opentelemetry",
|
|
118
|
+
"/v1/loop/opentelemetry/v1/traces",
|
|
95
119
|
"/api/v1/loop/opentelemetry",
|
|
120
|
+
"/v1/loop/opentelemetry",
|
|
96
121
|
"/v1/traces",
|
|
97
122
|
"/api/v1",
|
|
123
|
+
"/v1",
|
|
98
124
|
];
|
|
99
125
|
for (const suffix of suffixes) {
|
|
100
126
|
if (base.endsWith(suffix)) {
|
|
101
|
-
|
|
102
|
-
return base.slice(0, -trimSuffix.length).replace(/\/+$/, "");
|
|
127
|
+
return base.slice(0, -suffix.length).replace(/\/+$/, "");
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
130
|
return base;
|
|
@@ -195,7 +220,7 @@ function spanToUploadSpan(span, config) {
|
|
|
195
220
|
const attrs = span.attributes || {};
|
|
196
221
|
const { tags, systemTags } = splitAttributes(attrs);
|
|
197
222
|
const resourceAttrs = span.resource?.attributes || {};
|
|
198
|
-
const serviceName = resourceAttrs["service.name"] || config.serviceName || "
|
|
223
|
+
const serviceName = resourceAttrs["service.name"] || config.serviceName || "";
|
|
199
224
|
const spanType = mapSpanType(attrs["cozeloop.span_type"]);
|
|
200
225
|
const input = attrs["cozeloop.input"] !== undefined ? safeStringify(attrs["cozeloop.input"]) : "";
|
|
201
226
|
const output = attrs["cozeloop.output"] !== undefined ? safeStringify(attrs["cozeloop.output"]) : "";
|
|
@@ -204,7 +229,6 @@ function spanToUploadSpan(span, config) {
|
|
|
204
229
|
return {
|
|
205
230
|
started_at_micros: hrTimeToUnixMicros(span.startTime),
|
|
206
231
|
log_id: "",
|
|
207
|
-
traceId: spanContext.traceId,
|
|
208
232
|
trace_id: spanContext.traceId,
|
|
209
233
|
span_id: spanContext.spanId,
|
|
210
234
|
parent_id: span.parentSpanId || "0",
|
|
@@ -260,9 +284,11 @@ class CozeloopIngestExporter {
|
|
|
260
284
|
this.url = normalizeIngestUrl(config.endpoint || config.url);
|
|
261
285
|
this.headers = config.headers || {};
|
|
262
286
|
this.logger = config.logger;
|
|
287
|
+
this.logFile = config.logFile;
|
|
263
288
|
this.workspaceId = config.workspaceId;
|
|
264
289
|
this.serviceName = config.serviceName;
|
|
265
290
|
this.shutdownRequested = false;
|
|
291
|
+
fileLog(this.logFile, `[ingest] exporter ready url=${this.url} workspaceId=${this.workspaceId}`);
|
|
266
292
|
}
|
|
267
293
|
export(spans, resultCallback) {
|
|
268
294
|
if (!spans || spans.length === 0 || this.shutdownRequested) {
|
|
@@ -273,6 +299,7 @@ class CozeloopIngestExporter {
|
|
|
273
299
|
.then(() => resultCallback({ code: EXPORT_SUCCESS }))
|
|
274
300
|
.catch((err) => {
|
|
275
301
|
this.logger?.error?.(`[CozeloopTrace] CozeLoop ingest export failed: ${err?.message || err}`);
|
|
302
|
+
fileLog(this.logFile, `[ingest] export FAILED url=${this.url} spans=${spans.length} err=${err?.message || err}`);
|
|
276
303
|
resultCallback({ code: EXPORT_FAILED, error: err });
|
|
277
304
|
});
|
|
278
305
|
}
|
|
@@ -283,15 +310,18 @@ class CozeloopIngestExporter {
|
|
|
283
310
|
serviceName: this.serviceName,
|
|
284
311
|
})),
|
|
285
312
|
};
|
|
313
|
+
fileLog(this.logFile, `[ingest] POST url=${this.url} spans=${body.spans.length}`);
|
|
286
314
|
const res = await postJson(this.url, body, this.headers);
|
|
287
315
|
if (res.status < 200 || res.status >= 300) {
|
|
288
316
|
const snippet = String(res.body || "").slice(0, 300);
|
|
317
|
+
fileLog(this.logFile, `[ingest] HTTP ${res.status} url=${this.url}${snippet ? ` body=${snippet}` : ""}`);
|
|
289
318
|
throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
|
|
290
319
|
}
|
|
291
320
|
if (res.body) {
|
|
292
321
|
try {
|
|
293
322
|
const parsed = JSON.parse(res.body);
|
|
294
323
|
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
|
324
|
+
fileLog(this.logFile, `[ingest] biz-error code=${parsed.code} msg=${parsed.msg || ""} url=${this.url}`);
|
|
295
325
|
throw new Error(`code ${parsed.code}: ${parsed.msg || res.body.slice(0, 300)}`);
|
|
296
326
|
}
|
|
297
327
|
}
|
|
@@ -301,6 +331,7 @@ class CozeloopIngestExporter {
|
|
|
301
331
|
}
|
|
302
332
|
}
|
|
303
333
|
}
|
|
334
|
+
fileLog(this.logFile, `[ingest] OK HTTP ${res.status} spans=${body.spans.length}`);
|
|
304
335
|
}
|
|
305
336
|
async forceFlush() {
|
|
306
337
|
return;
|
|
@@ -405,13 +436,17 @@ export class CozeloopExporter {
|
|
|
405
436
|
const authorization = this.config.authorization;
|
|
406
437
|
const workspaceId = this.config.workspaceId;
|
|
407
438
|
this.api.logger.info(`[CozeloopTrace] Using authorization, workspaceId=${workspaceId}, tokenLength=${authorization?.length}`);
|
|
439
|
+
fileLog(this.config.logFile, `[init] ingestUrl=${normalizeIngestUrl(this.config.endpoint)} workspaceId=${workspaceId} tokenLength=${authorization?.length || 0} plugin=${PLUGIN_VERSION}`);
|
|
408
440
|
const exporter = new CozeloopIngestExporter({
|
|
409
441
|
endpoint: this.config.endpoint,
|
|
410
442
|
logger: this.api.logger,
|
|
443
|
+
logFile: this.config.logFile,
|
|
411
444
|
workspaceId,
|
|
412
445
|
serviceName: this.config.serviceName,
|
|
413
446
|
headers: {
|
|
414
447
|
"Authorization": authorization,
|
|
448
|
+
"User-Agent": `openclaw-cozeloop-trace/${PLUGIN_VERSION} node/${process.versions.node}`,
|
|
449
|
+
"X-Coze-Client-User-Agent": JSON.stringify(CLIENT_USER_AGENT),
|
|
415
450
|
"x-tt-env": "ppe_cozelab",
|
|
416
451
|
"x-use-ppe": "1",
|
|
417
452
|
},
|
|
@@ -473,10 +508,11 @@ export class CozeloopExporter {
|
|
|
473
508
|
const runtimeTag = {
|
|
474
509
|
language: "nodejs",
|
|
475
510
|
library: "openclaw",
|
|
511
|
+
scene: process.env.COZELOOP_SCENE || "custom",
|
|
512
|
+
library_version: null,
|
|
513
|
+
loop_sdk_version: `v${PLUGIN_VERSION}`,
|
|
514
|
+
extra: null,
|
|
476
515
|
};
|
|
477
|
-
if (process.env.COZELOOP_SCENE) {
|
|
478
|
-
runtimeTag.scene = process.env.COZELOOP_SCENE;
|
|
479
|
-
}
|
|
480
516
|
const systemTagRuntime = JSON.stringify(runtimeTag);
|
|
481
517
|
const span = this.tracer.startSpan(spanData.name, {
|
|
482
518
|
kind: spanKind,
|
|
@@ -579,10 +615,11 @@ export class CozeloopExporter {
|
|
|
579
615
|
const runtimeTag = {
|
|
580
616
|
language: "nodejs",
|
|
581
617
|
library: "openclaw",
|
|
618
|
+
scene: process.env.COZELOOP_SCENE || "custom",
|
|
619
|
+
library_version: null,
|
|
620
|
+
loop_sdk_version: `v${PLUGIN_VERSION}`,
|
|
621
|
+
extra: null,
|
|
582
622
|
};
|
|
583
|
-
if (process.env.COZELOOP_SCENE) {
|
|
584
|
-
runtimeTag.scene = process.env.COZELOOP_SCENE;
|
|
585
|
-
}
|
|
586
623
|
const systemTagRuntime = JSON.stringify(runtimeTag);
|
|
587
624
|
const span = this.tracer.startSpan(spanData.name, {
|
|
588
625
|
kind: spanKind,
|
|
@@ -534,6 +534,7 @@ const cozeloopTracePlugin = {
|
|
|
534
534
|
batchInterval: pluginConfig.batchInterval || 5000,
|
|
535
535
|
enabledHooks: pluginConfig.enabledHooks,
|
|
536
536
|
disableLocalCredentials: pluginConfig.disableLocalCredentials === true,
|
|
537
|
+
logFile: pluginConfig.logFile,
|
|
537
538
|
};
|
|
538
539
|
const exporter = new CozeloopExporter(api, config);
|
|
539
540
|
// per-agent trace 放行:traceAgentIds 为 onboard 写入的 allowlist(小写归一)。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-cozeloop-trace",
|
|
3
3
|
"name": "OpenClaw CozeLoop Trace",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.18",
|
|
5
5
|
"description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
|
|
6
6
|
"type": "plugin",
|
|
7
7
|
"entry": "./dist/index.js",
|
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
"type": "boolean",
|
|
55
55
|
"default": false,
|
|
56
56
|
"description": "Disable ~/.cozeloop credentials refresh and use configured authorization only"
|
|
57
|
+
},
|
|
58
|
+
"logFile": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"default": "",
|
|
61
|
+
"description": "Append trace ingest diagnostics (URL, span count, HTTP status, failure body) to this file"
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
}
|