coze_lab 0.1.31 → 0.1.33
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
|
@@ -5393,6 +5393,12 @@ function getCloudCozeloopApiBaseUrl() {
|
|
|
5393
5393
|
function normalizeCozeloopApiBaseUrl(raw) {
|
|
5394
5394
|
const base = raw.trim().replace(/\/+$/, '');
|
|
5395
5395
|
if (!base) return '';
|
|
5396
|
+
if (base.endsWith('/v1/loop/traces/ingest')) {
|
|
5397
|
+
return base.slice(0, -'/v1/loop/traces/ingest'.length).replace(/\/+$/, '');
|
|
5398
|
+
}
|
|
5399
|
+
if (base.endsWith('/api/v1/loop/traces/ingest')) {
|
|
5400
|
+
return base.slice(0, -'/v1/loop/traces/ingest'.length).replace(/\/+$/, '');
|
|
5401
|
+
}
|
|
5396
5402
|
if (base.endsWith('/v1/loop/opentelemetry/v1/traces')) {
|
|
5397
5403
|
return base.slice(0, -'/v1/loop/opentelemetry/v1/traces'.length).replace(/\/+$/, '');
|
|
5398
5404
|
}
|
|
@@ -5431,6 +5437,14 @@ function getOtelTracesUrl(cloud) {
|
|
|
5431
5437
|
return `${getOtelEndpointBase(cloud).replace(/\/+$/, '')}/v1/traces`;
|
|
5432
5438
|
}
|
|
5433
5439
|
|
|
5440
|
+
function getCozeloopIngestUrlFromBase(apiBase) {
|
|
5441
|
+
return `${String(apiBase || COZE_API).replace(/\/+$/, '')}/v1/loop/traces/ingest`;
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
function getCozeloopIngestUrl(cloud) {
|
|
5445
|
+
return getCozeloopIngestUrlFromBase(getCozeloopApiBaseUrl(cloud));
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5434
5448
|
function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud) {
|
|
5435
5449
|
if (!existing.plugins) existing.plugins = {};
|
|
5436
5450
|
if (!existing.plugins.allow) existing.plugins.allow = [];
|
|
@@ -5450,7 +5464,9 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud)
|
|
|
5450
5464
|
if (!existing.plugins.entries[PLUGIN].config) existing.plugins.entries[PLUGIN].config = {};
|
|
5451
5465
|
const pcfg = existing.plugins.entries[PLUGIN].config;
|
|
5452
5466
|
pcfg.authorization = `Bearer ${token}`;
|
|
5453
|
-
|
|
5467
|
+
// OpenClaw runtime uses CozeLoop ingest protocol, not OTLP. Keep accepting
|
|
5468
|
+
// OTEL_ENDPOINT in cloud, but normalize it to the API base URL before writing.
|
|
5469
|
+
pcfg.endpoint = getCozeloopApiBaseUrl(cloud);
|
|
5454
5470
|
pcfg.workspaceId = workspaceId;
|
|
5455
5471
|
pcfg.debug = true;
|
|
5456
5472
|
pcfg.disableLocalCredentials = !!cloud;
|
|
@@ -5506,7 +5522,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud) {
|
|
|
5506
5522
|
if (isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud)) {
|
|
5507
5523
|
ok(`OpenClaw plugin already configured in ${configPath}`);
|
|
5508
5524
|
info('OpenClaw gateway restart skipped (configuration unchanged).');
|
|
5509
|
-
return { configPath, pluginDir, unchanged: true };
|
|
5525
|
+
return { configPath, pluginDir, unchanged: true, gatewayRestarted: false, gatewayRestartSkipped: true };
|
|
5510
5526
|
}
|
|
5511
5527
|
|
|
5512
5528
|
// 1. Write plugin files to ~/.cozeloop/openclaw-plugin/
|
|
@@ -5575,11 +5591,11 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud) {
|
|
|
5575
5591
|
info('Restarting OpenClaw gateway to apply hook changes...');
|
|
5576
5592
|
execSync('openclaw gateway restart', { stdio: 'pipe' });
|
|
5577
5593
|
ok('OpenClaw gateway restarted');
|
|
5594
|
+
return { configPath, pluginDir, gatewayRestarted: true };
|
|
5578
5595
|
} catch (e) {
|
|
5579
5596
|
warn(`gateway restart 失败,请手动执行: openclaw gateway restart(${e.message})`);
|
|
5597
|
+
return { configPath, pluginDir, gatewayRestarted: false, gatewayRestartError: e.message };
|
|
5580
5598
|
}
|
|
5581
|
-
|
|
5582
|
-
return { configPath, pluginDir };
|
|
5583
5599
|
}
|
|
5584
5600
|
|
|
5585
5601
|
// ─── 8. Auth — Device Code OAuth + token store ───────────────────────────────
|
|
@@ -5848,48 +5864,50 @@ async function verifyOpenClawTraceLink(cloud) {
|
|
|
5848
5864
|
return { success: false, status: 0, body: 'plugin authorization missing' };
|
|
5849
5865
|
}
|
|
5850
5866
|
|
|
5851
|
-
// 1) 用插件【实际配置的】token 打一条最小
|
|
5852
|
-
// 不能发空 resourceSpans,CozeLoop 会返回 "unknown event type",导致自检假失败。
|
|
5867
|
+
// 1) 用插件【实际配置的】token 打一条最小 CozeLoop ingest trace ——这才是运行时真实用的那个 token。
|
|
5853
5868
|
const authHeader = pcfg.authorization; // 形如 "Bearer czu_xxx"
|
|
5854
|
-
const tracesUrl = (pcfg.endpoint
|
|
5855
|
-
? `${String(pcfg.endpoint).replace(/\/+$/, '')}/v1/traces`
|
|
5856
|
-
: getOtelTracesUrl(cloud));
|
|
5869
|
+
const tracesUrl = getCozeloopIngestUrlFromBase(normalizeCozeloopApiBaseUrl(pcfg.endpoint || getCozeloopApiBaseUrl(cloud)));
|
|
5857
5870
|
const workspaceId = pcfg.workspaceId || WORKSPACE_ID;
|
|
5858
5871
|
const traceId = crypto.randomBytes(16).toString('hex');
|
|
5859
5872
|
const spanId = crypto.randomBytes(8).toString('hex');
|
|
5860
|
-
const
|
|
5873
|
+
const nowMicros = Date.now() * 1000;
|
|
5861
5874
|
const pair = crypto.randomBytes(6).toString('hex');
|
|
5862
|
-
const
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5875
|
+
const ingestBody = {
|
|
5876
|
+
spans: [{
|
|
5877
|
+
started_at_micros: nowMicros,
|
|
5878
|
+
log_id: '',
|
|
5879
|
+
span_id: spanId,
|
|
5880
|
+
parent_id: '0',
|
|
5881
|
+
trace_id: traceId,
|
|
5882
|
+
duration_micros: 1,
|
|
5883
|
+
service_name: 'cozelab-onboard-openclaw',
|
|
5884
|
+
workspace_id: String(workspaceId),
|
|
5885
|
+
span_name: 'cozelab-onboard-openclaw-selfcheck',
|
|
5886
|
+
span_type: 'main',
|
|
5887
|
+
status_code: 0,
|
|
5888
|
+
input: 'cozelab-onboard-openclaw selfcheck',
|
|
5889
|
+
output: 'ok',
|
|
5890
|
+
object_storage: '',
|
|
5891
|
+
system_tags_string: {
|
|
5892
|
+
runtime: JSON.stringify({ language: 'nodejs', library: 'openclaw', scene: process.env.COZELOOP_SCENE || 'custom' }),
|
|
5868
5893
|
},
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
status: { code: 1 },
|
|
5879
|
-
attributes: [
|
|
5880
|
-
{ key: 'pair_code', value: { stringValue: pair } },
|
|
5881
|
-
{ key: 'source', value: { stringValue: 'cozelab-onboard-openclaw' } },
|
|
5882
|
-
],
|
|
5883
|
-
}],
|
|
5884
|
-
}],
|
|
5894
|
+
system_tags_long: {},
|
|
5895
|
+
system_tags_double: {},
|
|
5896
|
+
tags_string: {
|
|
5897
|
+
pair_code: pair,
|
|
5898
|
+
source: 'cozelab-onboard-openclaw',
|
|
5899
|
+
},
|
|
5900
|
+
tags_long: {},
|
|
5901
|
+
tags_double: {},
|
|
5902
|
+
tags_bool: {},
|
|
5885
5903
|
}],
|
|
5886
5904
|
};
|
|
5887
5905
|
let res;
|
|
5888
5906
|
try {
|
|
5889
5907
|
res = await httpsPost(
|
|
5890
5908
|
tracesUrl,
|
|
5891
|
-
|
|
5892
|
-
{ Authorization: authHeader
|
|
5909
|
+
ingestBody,
|
|
5910
|
+
{ Authorization: authHeader },
|
|
5893
5911
|
);
|
|
5894
5912
|
} catch (e) {
|
|
5895
5913
|
warn(`openclaw 插件 token 上报探测失败: ${e.message}`);
|
|
@@ -6221,9 +6239,18 @@ function authStatus() {
|
|
|
6221
6239
|
const NEXT_STEP = {
|
|
6222
6240
|
'claude-code': 'Hook 已写入。Claude Code 会自动热重载 hooks,当前会话即刻生效,无需 /new 或重启。',
|
|
6223
6241
|
'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex 会话(已配置 SessionStart hook,新会话自动加载)。',
|
|
6224
|
-
'openclaw': 'OpenClaw gateway 已自动重启,trace 即刻生效。',
|
|
6225
6242
|
};
|
|
6226
6243
|
|
|
6244
|
+
function openClawNextStep(written) {
|
|
6245
|
+
if (written?.gatewayRestarted) {
|
|
6246
|
+
return 'OpenClaw gateway 已自动重启,trace 即刻生效。';
|
|
6247
|
+
}
|
|
6248
|
+
if (written?.gatewayRestartSkipped) {
|
|
6249
|
+
return 'OpenClaw 配置未变化,gateway restart 已跳过。';
|
|
6250
|
+
}
|
|
6251
|
+
return 'OpenClaw gateway 未能自动重启;请手动执行 openclaw gateway restart 后生效。';
|
|
6252
|
+
}
|
|
6253
|
+
|
|
6227
6254
|
async function main() {
|
|
6228
6255
|
console.log('');
|
|
6229
6256
|
info(`CozeLoop Onboard CLI starting... (coze_lab v${PACKAGE_VERSION})`);
|
|
@@ -6422,7 +6449,7 @@ async function main() {
|
|
|
6422
6449
|
summaryLines.push(`Config: ~/.openclaw/openclaw.json`);
|
|
6423
6450
|
}
|
|
6424
6451
|
summaryLines.push('');
|
|
6425
|
-
summaryLines.push(NEXT_STEP[agent]);
|
|
6452
|
+
summaryLines.push(agent === 'openclaw' ? openClawNextStep(written) : NEXT_STEP[agent]);
|
|
6426
6453
|
|
|
6427
6454
|
// Enterprise policy note for Claude Code
|
|
6428
6455
|
if (agent === 'claude-code') {
|
package/package.json
CHANGED
|
@@ -80,125 +80,150 @@ async function getRefreshedToken(currentAuthorization, opts = {}) {
|
|
|
80
80
|
|
|
81
81
|
const EXPORT_SUCCESS = 0;
|
|
82
82
|
const EXPORT_FAILED = 1;
|
|
83
|
+
const INGEST_TRACE_PATH = "/v1/loop/traces/ingest";
|
|
83
84
|
|
|
84
|
-
function
|
|
85
|
+
function normalizeApiBaseUrl(endpoint) {
|
|
85
86
|
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
86
|
-
|
|
87
|
+
if (!base)
|
|
88
|
+
return _COZE_API;
|
|
89
|
+
const suffixes = [
|
|
90
|
+
"/v1/loop/traces/ingest",
|
|
91
|
+
"/api/v1/loop/traces/ingest",
|
|
92
|
+
"/v1/loop/opentelemetry/v1/traces",
|
|
93
|
+
"/api/v1/loop/opentelemetry/v1/traces",
|
|
94
|
+
"/v1/loop/opentelemetry",
|
|
95
|
+
"/api/v1/loop/opentelemetry",
|
|
96
|
+
"/v1/traces",
|
|
97
|
+
"/api/v1",
|
|
98
|
+
];
|
|
99
|
+
for (const suffix of suffixes) {
|
|
100
|
+
if (base.endsWith(suffix)) {
|
|
101
|
+
const trimSuffix = suffix.startsWith("/api/") ? suffix.slice("/api".length) : suffix;
|
|
102
|
+
return base.slice(0, -trimSuffix.length).replace(/\/+$/, "");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return base;
|
|
87
106
|
}
|
|
88
107
|
|
|
89
|
-
function
|
|
108
|
+
function normalizeIngestUrl(endpoint) {
|
|
109
|
+
const base = String(endpoint || "").replace(/\/+$/, "");
|
|
110
|
+
if (base.endsWith(INGEST_TRACE_PATH))
|
|
111
|
+
return base;
|
|
112
|
+
return `${normalizeApiBaseUrl(base)}${INGEST_TRACE_PATH}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hrTimeToUnixMicros(time) {
|
|
90
116
|
if (Array.isArray(time)) {
|
|
91
|
-
return (BigInt(time[0]) *
|
|
117
|
+
return Number(BigInt(time[0]) * 1000000n + BigInt(Math.trunc(time[1] / 1000)));
|
|
92
118
|
}
|
|
93
119
|
const millis = time instanceof Date ? time.getTime() : Number(time || Date.now());
|
|
94
|
-
return
|
|
120
|
+
return Math.trunc(millis * 1000);
|
|
95
121
|
}
|
|
96
122
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
case SpanKind.SERVER: return 2;
|
|
101
|
-
case SpanKind.CLIENT: return 3;
|
|
102
|
-
case SpanKind.PRODUCER: return 4;
|
|
103
|
-
case SpanKind.CONSUMER: return 5;
|
|
104
|
-
default: return 0;
|
|
123
|
+
function hrDurationToMicros(duration) {
|
|
124
|
+
if (Array.isArray(duration)) {
|
|
125
|
+
return Number(BigInt(duration[0]) * 1000000n + BigInt(Math.trunc(duration[1] / 1000)));
|
|
105
126
|
}
|
|
127
|
+
return 0;
|
|
106
128
|
}
|
|
107
129
|
|
|
108
|
-
function
|
|
130
|
+
function safeStringify(value) {
|
|
109
131
|
if (value === undefined || value === null)
|
|
110
|
-
return
|
|
132
|
+
return "";
|
|
111
133
|
if (typeof value === "string")
|
|
112
|
-
return
|
|
113
|
-
if (typeof value === "boolean")
|
|
114
|
-
return { boolValue: value };
|
|
115
|
-
if (typeof value === "number") {
|
|
116
|
-
if (Number.isInteger(value))
|
|
117
|
-
return { intValue: String(value) };
|
|
118
|
-
return { doubleValue: value };
|
|
119
|
-
}
|
|
134
|
+
return value;
|
|
120
135
|
try {
|
|
121
|
-
return
|
|
136
|
+
return JSON.stringify(value);
|
|
122
137
|
}
|
|
123
138
|
catch {
|
|
124
|
-
return
|
|
139
|
+
return String(value);
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
142
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
function mapSpanType(type) {
|
|
144
|
+
switch (type) {
|
|
145
|
+
case "entry":
|
|
146
|
+
case "gateway":
|
|
147
|
+
case "message":
|
|
148
|
+
case "session":
|
|
149
|
+
return "main";
|
|
150
|
+
case "agent":
|
|
151
|
+
case "model":
|
|
152
|
+
case "tool":
|
|
153
|
+
return type;
|
|
154
|
+
default:
|
|
155
|
+
return type || "main";
|
|
132
156
|
}
|
|
133
|
-
return scalarToAnyValue(value);
|
|
134
157
|
}
|
|
135
158
|
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
159
|
+
function putTypedTag(maps, key, value) {
|
|
160
|
+
if (value === undefined || value === null || key === "")
|
|
161
|
+
return;
|
|
162
|
+
if (key === "cozeloop.span_type" || key === "cozeloop.input" || key === "cozeloop.output" ||
|
|
163
|
+
key === "cozeloop.system_tag_runtime") {
|
|
164
|
+
return;
|
|
143
165
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const otlpSpan = {
|
|
150
|
-
traceId: spanContext.traceId,
|
|
151
|
-
spanId: spanContext.spanId,
|
|
152
|
-
name: span.name,
|
|
153
|
-
kind: spanKindToOtlp(span.kind),
|
|
154
|
-
startTimeUnixNano: hrTimeToUnixNano(span.startTime),
|
|
155
|
-
endTimeUnixNano: hrTimeToUnixNano(span.endTime),
|
|
156
|
-
attributes: attributesToOtlp(span.attributes),
|
|
157
|
-
status: { code: span.status?.code ?? 0 },
|
|
158
|
-
};
|
|
159
|
-
if (span.parentSpanId) {
|
|
160
|
-
otlpSpan.parentSpanId = span.parentSpanId;
|
|
166
|
+
if (typeof value === "boolean") {
|
|
167
|
+
maps.bool[key] = value;
|
|
168
|
+
}
|
|
169
|
+
else if (typeof value === "number" && Number.isInteger(value)) {
|
|
170
|
+
maps.long[key] = value;
|
|
161
171
|
}
|
|
162
|
-
if (
|
|
163
|
-
|
|
172
|
+
else if (typeof value === "number") {
|
|
173
|
+
maps.double[key] = value;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
maps.string[key] = typeof value === "string" ? value : safeStringify(value);
|
|
164
177
|
}
|
|
165
|
-
return otlpSpan;
|
|
166
178
|
}
|
|
167
179
|
|
|
168
|
-
function
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
version: scope.version || PLUGIN_VERSION,
|
|
176
|
-
};
|
|
177
|
-
if (scope.schemaUrl) {
|
|
178
|
-
scopeData.schemaUrl = scope.schemaUrl;
|
|
179
|
-
}
|
|
180
|
-
const resourceKey = JSON.stringify(resourceAttrs);
|
|
181
|
-
let resourceGroup = resourceGroups.get(resourceKey);
|
|
182
|
-
if (!resourceGroup) {
|
|
183
|
-
resourceGroup = {
|
|
184
|
-
resource: { attributes: resourceAttrs },
|
|
185
|
-
scopeGroups: new Map(),
|
|
186
|
-
};
|
|
187
|
-
resourceGroups.set(resourceKey, resourceGroup);
|
|
188
|
-
}
|
|
189
|
-
const scopeKey = JSON.stringify(scopeData);
|
|
190
|
-
let scopeGroup = resourceGroup.scopeGroups.get(scopeKey);
|
|
191
|
-
if (!scopeGroup) {
|
|
192
|
-
scopeGroup = { scope: scopeData, spans: [] };
|
|
193
|
-
resourceGroup.scopeGroups.set(scopeKey, scopeGroup);
|
|
180
|
+
function splitAttributes(attributes) {
|
|
181
|
+
const tags = { string: {}, long: {}, double: {}, bool: {} };
|
|
182
|
+
const systemTags = { string: {}, long: {}, double: {} };
|
|
183
|
+
for (const [key, value] of Object.entries(attributes || {})) {
|
|
184
|
+
if (key === "cozeloop.system_tag_runtime") {
|
|
185
|
+
systemTags.string.runtime = safeStringify(value);
|
|
186
|
+
continue;
|
|
194
187
|
}
|
|
195
|
-
|
|
188
|
+
putTypedTag(tags, key, value);
|
|
196
189
|
}
|
|
190
|
+
return { tags, systemTags };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function spanToUploadSpan(span, config) {
|
|
194
|
+
const spanContext = span.spanContext();
|
|
195
|
+
const attrs = span.attributes || {};
|
|
196
|
+
const { tags, systemTags } = splitAttributes(attrs);
|
|
197
|
+
const resourceAttrs = span.resource?.attributes || {};
|
|
198
|
+
const serviceName = resourceAttrs["service.name"] || config.serviceName || "openclaw-agent";
|
|
199
|
+
const spanType = mapSpanType(attrs["cozeloop.span_type"]);
|
|
200
|
+
const input = attrs["cozeloop.input"] !== undefined ? safeStringify(attrs["cozeloop.input"]) : "";
|
|
201
|
+
const output = attrs["cozeloop.output"] !== undefined ? safeStringify(attrs["cozeloop.output"]) : "";
|
|
202
|
+
const hasError = span.status?.code === SpanStatusCode.ERROR || attrs.error === true || attrs["tool.error"] === true;
|
|
203
|
+
|
|
197
204
|
return {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
started_at_micros: hrTimeToUnixMicros(span.startTime),
|
|
206
|
+
log_id: "",
|
|
207
|
+
traceId: spanContext.traceId,
|
|
208
|
+
trace_id: spanContext.traceId,
|
|
209
|
+
span_id: spanContext.spanId,
|
|
210
|
+
parent_id: span.parentSpanId || "0",
|
|
211
|
+
duration_micros: hrDurationToMicros(span.duration),
|
|
212
|
+
service_name: String(serviceName),
|
|
213
|
+
workspace_id: String(config.workspaceId || ""),
|
|
214
|
+
span_name: String(span.name || "unknown"),
|
|
215
|
+
span_type: spanType,
|
|
216
|
+
status_code: hasError ? -1 : 0,
|
|
217
|
+
input,
|
|
218
|
+
output,
|
|
219
|
+
object_storage: "",
|
|
220
|
+
system_tags_string: systemTags.string,
|
|
221
|
+
system_tags_long: systemTags.long,
|
|
222
|
+
system_tags_double: systemTags.double,
|
|
223
|
+
tags_string: tags.string,
|
|
224
|
+
tags_long: tags.long,
|
|
225
|
+
tags_double: tags.double,
|
|
226
|
+
tags_bool: tags.bool,
|
|
202
227
|
};
|
|
203
228
|
}
|
|
204
229
|
|
|
@@ -224,17 +249,19 @@ function postJson(url, body, headers) {
|
|
|
224
249
|
res.on("end", () => resolve({ status: res.statusCode || 0, body: buf }));
|
|
225
250
|
});
|
|
226
251
|
req.on("error", reject);
|
|
227
|
-
req.setTimeout(15000, () => req.destroy(new Error("
|
|
252
|
+
req.setTimeout(15000, () => req.destroy(new Error("CozeLoop ingest export timed out")));
|
|
228
253
|
req.write(data);
|
|
229
254
|
req.end();
|
|
230
255
|
});
|
|
231
256
|
}
|
|
232
257
|
|
|
233
|
-
class
|
|
258
|
+
class CozeloopIngestExporter {
|
|
234
259
|
constructor(config) {
|
|
235
|
-
this.url =
|
|
260
|
+
this.url = normalizeIngestUrl(config.endpoint || config.url);
|
|
236
261
|
this.headers = config.headers || {};
|
|
237
262
|
this.logger = config.logger;
|
|
263
|
+
this.workspaceId = config.workspaceId;
|
|
264
|
+
this.serviceName = config.serviceName;
|
|
238
265
|
this.shutdownRequested = false;
|
|
239
266
|
}
|
|
240
267
|
export(spans, resultCallback) {
|
|
@@ -245,17 +272,35 @@ class JsonOtlpTraceExporter {
|
|
|
245
272
|
this.postSpans(spans)
|
|
246
273
|
.then(() => resultCallback({ code: EXPORT_SUCCESS }))
|
|
247
274
|
.catch((err) => {
|
|
248
|
-
this.logger?.error?.(`[CozeloopTrace]
|
|
275
|
+
this.logger?.error?.(`[CozeloopTrace] CozeLoop ingest export failed: ${err?.message || err}`);
|
|
249
276
|
resultCallback({ code: EXPORT_FAILED, error: err });
|
|
250
277
|
});
|
|
251
278
|
}
|
|
252
279
|
async postSpans(spans) {
|
|
253
|
-
const body =
|
|
280
|
+
const body = {
|
|
281
|
+
spans: spans.map((span) => spanToUploadSpan(span, {
|
|
282
|
+
workspaceId: this.workspaceId,
|
|
283
|
+
serviceName: this.serviceName,
|
|
284
|
+
})),
|
|
285
|
+
};
|
|
254
286
|
const res = await postJson(this.url, body, this.headers);
|
|
255
287
|
if (res.status < 200 || res.status >= 300) {
|
|
256
288
|
const snippet = String(res.body || "").slice(0, 300);
|
|
257
289
|
throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
|
|
258
290
|
}
|
|
291
|
+
if (res.body) {
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(res.body);
|
|
294
|
+
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
|
295
|
+
throw new Error(`code ${parsed.code}: ${parsed.msg || res.body.slice(0, 300)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (/^code\s/.test(String(err?.message || ""))) {
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
259
304
|
}
|
|
260
305
|
async forceFlush() {
|
|
261
306
|
return;
|
|
@@ -360,12 +405,13 @@ export class CozeloopExporter {
|
|
|
360
405
|
const authorization = this.config.authorization;
|
|
361
406
|
const workspaceId = this.config.workspaceId;
|
|
362
407
|
this.api.logger.info(`[CozeloopTrace] Using authorization, workspaceId=${workspaceId}, tokenLength=${authorization?.length}`);
|
|
363
|
-
const exporter = new
|
|
364
|
-
|
|
408
|
+
const exporter = new CozeloopIngestExporter({
|
|
409
|
+
endpoint: this.config.endpoint,
|
|
365
410
|
logger: this.api.logger,
|
|
411
|
+
workspaceId,
|
|
412
|
+
serviceName: this.config.serviceName,
|
|
366
413
|
headers: {
|
|
367
414
|
"Authorization": authorization,
|
|
368
|
-
"cozeloop-workspace-id": workspaceId,
|
|
369
415
|
"x-tt-env": "ppe_cozelab",
|
|
370
416
|
"x-use-ppe": "1",
|
|
371
417
|
},
|
|
@@ -476,6 +476,19 @@ function resolveCozeContext(input, sessionId) {
|
|
|
476
476
|
return cozeContextBySession.get(sessionId);
|
|
477
477
|
return lastCozeContext || {};
|
|
478
478
|
}
|
|
479
|
+
function resetRuntimeState() {
|
|
480
|
+
lastUserChannelId = undefined;
|
|
481
|
+
lastUserTraceContext = undefined;
|
|
482
|
+
lastOpenclawSessionId = undefined;
|
|
483
|
+
cozeContextBySession.clear();
|
|
484
|
+
lastCozeContext = undefined;
|
|
485
|
+
activeAgentCtx = undefined;
|
|
486
|
+
activeAgentChannelId = undefined;
|
|
487
|
+
activeAgentContextByKey.clear();
|
|
488
|
+
lastUserInput = undefined;
|
|
489
|
+
lastUserContextByKey.clear();
|
|
490
|
+
pendingToolCallsByKey.clear();
|
|
491
|
+
}
|
|
479
492
|
// Active agent context: set in before_agent_start, cleared in agent_end.
|
|
480
493
|
// All hooks between these two (llm_input, llm_output, tool calls, messages)
|
|
481
494
|
// use this to ensure every span lands in the same Trace.
|
|
@@ -503,6 +516,7 @@ const cozeloopTracePlugin = {
|
|
|
503
516
|
version: PLUGIN_VERSION,
|
|
504
517
|
description: "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
|
|
505
518
|
activate(api) {
|
|
519
|
+
resetRuntimeState();
|
|
506
520
|
const pluginConfig = api.pluginConfig || {};
|
|
507
521
|
const authorization = pluginConfig.authorization;
|
|
508
522
|
const workspaceId = pluginConfig.workspaceId;
|
|
@@ -511,7 +525,7 @@ const cozeloopTracePlugin = {
|
|
|
511
525
|
return;
|
|
512
526
|
}
|
|
513
527
|
const config = {
|
|
514
|
-
endpoint: pluginConfig.endpoint || "https://api.coze.cn
|
|
528
|
+
endpoint: pluginConfig.endpoint || "https://api.coze.cn",
|
|
515
529
|
authorization,
|
|
516
530
|
workspaceId,
|
|
517
531
|
serviceName: pluginConfig.serviceName || "openclaw-agent",
|
|
@@ -573,6 +587,21 @@ const cozeloopTracePlugin = {
|
|
|
573
587
|
const lastSessionId = lastUserTraceContext?.openclawSessionId || sessionIdFromChannelId(lastUserChannelId);
|
|
574
588
|
return !rawSessionId || !lastSessionId || rawSessionId === lastSessionId;
|
|
575
589
|
};
|
|
590
|
+
const canFallbackToActiveAgentContext = (rawChannelId) => {
|
|
591
|
+
const rawSessionId = sessionIdFromChannelId(rawChannelId);
|
|
592
|
+
const activeSessionId = activeAgentCtx?.openclawSessionId || sessionIdFromChannelId(activeAgentChannelId);
|
|
593
|
+
return !rawSessionId || !activeSessionId || rawSessionId === activeSessionId;
|
|
594
|
+
};
|
|
595
|
+
const canUseLastUserFallbackFor = (ctx, rawChannelId) => {
|
|
596
|
+
if (!lastUserTraceContext || lastUserTraceContext === ctx)
|
|
597
|
+
return true;
|
|
598
|
+
const rawSessionId = ctx?.openclawSessionId || sessionIdFromChannelId(rawChannelId);
|
|
599
|
+
const lastSessionId = lastUserTraceContext.openclawSessionId || sessionIdFromChannelId(lastUserChannelId);
|
|
600
|
+
return !rawSessionId || !lastSessionId || rawSessionId === lastSessionId;
|
|
601
|
+
};
|
|
602
|
+
const lastUserFallbackFor = (ctx, rawChannelId) => {
|
|
603
|
+
return canUseLastUserFallbackFor(ctx, rawChannelId) ? lastUserTraceContext : undefined;
|
|
604
|
+
};
|
|
576
605
|
const startTurn = (runId, channelId, originalChannelId, openclawSessionId) => {
|
|
577
606
|
const traceId = generateId(32);
|
|
578
607
|
const ctx = {
|
|
@@ -598,7 +627,7 @@ const cozeloopTracePlugin = {
|
|
|
598
627
|
const getOrCreateContext = (rawChannelId, runId, hookName) => {
|
|
599
628
|
let channelId = rawChannelId;
|
|
600
629
|
let activeCtx = getContextByChannel(rawChannelId);
|
|
601
|
-
const effectiveRunId = runId || activeCtx?.runId || `run-${Date.now()}`;
|
|
630
|
+
const effectiveRunId = runId || activeCtx?.runId || `run-${Date.now()}-${generateId(8)}`;
|
|
602
631
|
if (rawChannelId.startsWith("agent/") && effectiveRunId) {
|
|
603
632
|
const originalChannelId = getOriginalChannelId(effectiveRunId);
|
|
604
633
|
if (originalChannelId) {
|
|
@@ -656,7 +685,9 @@ const cozeloopTracePlugin = {
|
|
|
656
685
|
return found;
|
|
657
686
|
}
|
|
658
687
|
if (lastUserTraceContext) {
|
|
659
|
-
|
|
688
|
+
if (canFallbackToLastUserContext(channelId)) {
|
|
689
|
+
return { ctx: lastUserTraceContext, channelId: lastUserChannelId || channelId };
|
|
690
|
+
}
|
|
660
691
|
}
|
|
661
692
|
return undefined;
|
|
662
693
|
};
|
|
@@ -674,7 +705,9 @@ const cozeloopTracePlugin = {
|
|
|
674
705
|
return found;
|
|
675
706
|
}
|
|
676
707
|
if (activeAgentCtx) {
|
|
677
|
-
|
|
708
|
+
if (canFallbackToActiveAgentContext(channelId)) {
|
|
709
|
+
return { ctx: activeAgentCtx, channelId: activeAgentChannelId || channelId };
|
|
710
|
+
}
|
|
678
711
|
}
|
|
679
712
|
return undefined;
|
|
680
713
|
};
|
|
@@ -1338,14 +1371,15 @@ const cozeloopTracePlugin = {
|
|
|
1338
1371
|
}
|
|
1339
1372
|
const rootSpanId = ctx.rootSpanId;
|
|
1340
1373
|
const rootSpanStartTime = ctx.rootSpanStartTime;
|
|
1341
|
-
const
|
|
1374
|
+
const lastUserFallback = lastUserFallbackFor(ctx, channelId);
|
|
1375
|
+
const userInput = ctx.userInput || lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1342
1376
|
const traceId = ctx.traceId;
|
|
1343
1377
|
const hasRootSpan = !!rootSpanStartTime;
|
|
1344
1378
|
const savedLastUserChannelId = lastUserChannelId;
|
|
1345
1379
|
const originalChannelId = ctx.originalChannelId || channelId;
|
|
1346
1380
|
setTimeout(async () => {
|
|
1347
1381
|
if (hasRootSpan) {
|
|
1348
|
-
const finalOutput = ctx.lastOutput ||
|
|
1382
|
+
const finalOutput = ctx.lastOutput || lastUserFallback?.lastOutput;
|
|
1349
1383
|
if (config.debug) {
|
|
1350
1384
|
api.logger.info(`[CozeloopTrace] Ending root span with input=${userInput ? 'present' : 'missing'}, output=${finalOutput ? 'present' : 'missing'}`);
|
|
1351
1385
|
}
|
|
@@ -1390,8 +1424,9 @@ const cozeloopTracePlugin = {
|
|
|
1390
1424
|
// currentRootContext is set and the agent span becomes a proper child.
|
|
1391
1425
|
const ensureRootSpan = async (ctx, channelId) => {
|
|
1392
1426
|
// 心跳轮询消息:不是真实对话,整条 trace 不上报(沿用下方“无 coze-context 即 return”的同款范式)。
|
|
1427
|
+
const lastUserFallback = lastUserFallbackFor(ctx, channelId);
|
|
1393
1428
|
const heartbeatInput = ctx.userInput
|
|
1394
|
-
||
|
|
1429
|
+
|| lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1395
1430
|
if (isHeartbeatInput(heartbeatInput)) {
|
|
1396
1431
|
if (config.debug) {
|
|
1397
1432
|
api.logger.info(`[CozeloopTrace] skip heartbeat poll trace, traceId=${ctx.traceId}`);
|
|
@@ -1408,7 +1443,7 @@ const cozeloopTracePlugin = {
|
|
|
1408
1443
|
let cozeCtx = resolveCozeContext(ctx.userInput, ocSessionId);
|
|
1409
1444
|
if (Object.keys(cozeCtx).length === 0) {
|
|
1410
1445
|
// Try the fallback user inputs too before giving up.
|
|
1411
|
-
const fallbackInput =
|
|
1446
|
+
const fallbackInput = lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1412
1447
|
cozeCtx = resolveCozeContext(fallbackInput, ocSessionId);
|
|
1413
1448
|
}
|
|
1414
1449
|
if (Object.keys(cozeCtx).length === 0) {
|
|
@@ -1439,7 +1474,7 @@ const cozeloopTracePlugin = {
|
|
|
1439
1474
|
// Resolve user input: prefer ctx.userInput set by this turn's
|
|
1440
1475
|
// message_received, fall back to lastUserTraceContext, then lastUserInput.
|
|
1441
1476
|
if (!ctx.userInput) {
|
|
1442
|
-
ctx.userInput =
|
|
1477
|
+
ctx.userInput = lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
|
|
1443
1478
|
}
|
|
1444
1479
|
const rootSpanData = {
|
|
1445
1480
|
name: "openclaw_request",
|
|
@@ -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.16",
|
|
5
5
|
"description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
|
|
6
6
|
"type": "plugin",
|
|
7
7
|
"entry": "./dist/index.js",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"properties": {
|
|
11
11
|
"endpoint": {
|
|
12
12
|
"type": "string",
|
|
13
|
-
"default": "https://api.coze.cn
|
|
14
|
-
"description": "CozeLoop
|
|
13
|
+
"default": "https://api.coze.cn",
|
|
14
|
+
"description": "CozeLoop API base URL"
|
|
15
15
|
},
|
|
16
16
|
"authorization": {
|
|
17
17
|
"type": "string",
|