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