coze_lab 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/index.js +182 -20
- package/package.json +1 -1
- package/scripts/claude-code/cozeloop_hook.py +30 -4
- package/scripts/codex/cozeloop_hook.py +6 -7
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ npx coze_lab --logout # Clear cached credentials
|
|
|
24
24
|
|-----------|----------|-----------------|
|
|
25
25
|
| `--agent` | ✓ (for setup) | `claude-code`, `codex`, `openclaw` |
|
|
26
26
|
| `--agent-id` | — | Resolve `~/.coze/agents/<agentId>/config.json` and write per-agent config |
|
|
27
|
-
| `--cloud` | — | Cloud mode: read token from env and emit `COZE_LAB_RESULT=...` |
|
|
27
|
+
| `--cloud` | — | Cloud mode: read trace token from env and emit `COZE_LAB_RESULT=...` |
|
|
28
28
|
| `--codex-home` | — | Override Codex config home for non-cloud/custom runs |
|
|
29
29
|
| `--login` | — | Run the Device Code login flow only |
|
|
30
30
|
| `--status` | — | Print local token status (valid / expiring / expired) |
|
|
@@ -70,6 +70,12 @@ created after a new Codex turn, Codex did not load or execute the hook.
|
|
|
70
70
|
|
|
71
71
|
OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
|
|
72
72
|
|
|
73
|
+
In cloud mode, trace verification and hook uploads require
|
|
74
|
+
`COZELOOP_API_TOKEN`. `COZE_API_TOKEN` is kept only as a legacy compatibility
|
|
75
|
+
signal for hook injection; it is not treated as a CozeLoop SDK trace ingest
|
|
76
|
+
token. If cloud only provides `COZE_API_TOKEN`, onboard still writes the hook
|
|
77
|
+
configuration but reports `verify=fail` with `token_source=COZE_API_TOKEN`.
|
|
78
|
+
|
|
73
79
|
**At hook execution time** (Claude Code / Codex), the Python hook script automatically:
|
|
74
80
|
1. Reads `~/.cozeloop/credentials.json`
|
|
75
81
|
2. If the token expires in < 10 minutes, calls the Coze refresh API
|
package/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
|
|
|
17
17
|
// 云端(--cloud)模式:在 stdout 输出一行机器可读结果 COZE_LAB_RESULT={...},
|
|
18
18
|
// 供管理后台解析判定(inject/verify/logid/message),不依赖中文文案。
|
|
19
19
|
let CLOUD_MODE = false;
|
|
20
|
-
const cloudResult = { version: PACKAGE_VERSION, inject: 'skip', verify: 'skip', logid: '', message: '' };
|
|
20
|
+
const cloudResult = { version: PACKAGE_VERSION, inject: 'skip', verify: 'skip', logid: '', message: '', token_source: '' };
|
|
21
21
|
|
|
22
22
|
// errorBox 在云端模式下抛此异常(而非 process.exit),由 main 外层统一收尾。
|
|
23
23
|
class CloudAbort extends Error {}
|
|
@@ -40,6 +40,22 @@ function emitCloudResult() {
|
|
|
40
40
|
console.log('COZE_LAB_RESULT=' + JSON.stringify(safe));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function readEnv(name) {
|
|
44
|
+
return String(process.env[name] || '').trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getCloudTokenInfo() {
|
|
48
|
+
const loopToken = readEnv('COZELOOP_API_TOKEN');
|
|
49
|
+
if (loopToken) {
|
|
50
|
+
return { token: loopToken, source: 'COZELOOP_API_TOKEN', traceUsable: true };
|
|
51
|
+
}
|
|
52
|
+
const cozeToken = readEnv('COZE_API_TOKEN');
|
|
53
|
+
if (cozeToken) {
|
|
54
|
+
return { token: cozeToken, source: 'COZE_API_TOKEN', traceUsable: false };
|
|
55
|
+
}
|
|
56
|
+
return { token: '', source: '', traceUsable: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
// ─── 1. Color helpers ────────────────────────────────────────────────────────
|
|
44
60
|
const C = {
|
|
45
61
|
reset: '\x1b[0m',
|
|
@@ -4472,8 +4488,6 @@ function shellEnvLine(key, value) {
|
|
|
4472
4488
|
// writeClaudeCodeHook 配置 Claude Code 的 hook。
|
|
4473
4489
|
// configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
|
|
4474
4490
|
// settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
|
|
4475
|
-
// cloud=true 时不把明文 token 写进 settings.local.json —— 云端 hook 运行时直接读
|
|
4476
|
-
// 环境变量 COZE_API_TOKEN(见 scripts/claude-code/cozeloop_hook.py 的 get_fresh_token)。
|
|
4477
4491
|
function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cloud) {
|
|
4478
4492
|
const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
|
|
4479
4493
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
@@ -4527,12 +4541,28 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
4527
4541
|
const localSettings = mergeJson(localSettingsPath, (existing) => {
|
|
4528
4542
|
if (!existing.env) existing.env = {};
|
|
4529
4543
|
existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
|
|
4530
|
-
// 云端:token 由 sandbox 注入到环境变量 COZE_API_TOKEN,hook 运行时直接读,
|
|
4531
|
-
// 不在配置文件落明文 token。本地:写入 OAuth 拿到的 token。
|
|
4532
4544
|
if (cloud) {
|
|
4533
|
-
|
|
4545
|
+
const loopToken = readEnv('COZELOOP_API_TOKEN');
|
|
4546
|
+
const cozeToken = readEnv('COZE_API_TOKEN');
|
|
4547
|
+
if (loopToken) {
|
|
4548
|
+
existing.env.COZELOOP_API_TOKEN = loopToken;
|
|
4549
|
+
delete existing.env.COZE_API_TOKEN;
|
|
4550
|
+
} else if (cozeToken) {
|
|
4551
|
+
existing.env.COZE_API_TOKEN = cozeToken;
|
|
4552
|
+
delete existing.env.COZELOOP_API_TOKEN;
|
|
4553
|
+
}
|
|
4534
4554
|
} else {
|
|
4535
4555
|
existing.env.COZELOOP_API_TOKEN = patToken;
|
|
4556
|
+
delete existing.env.COZE_API_TOKEN;
|
|
4557
|
+
}
|
|
4558
|
+
const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
|
|
4559
|
+
const otelEndpoint = readEnv('OTEL_ENDPOINT');
|
|
4560
|
+
if (loopBaseUrl) {
|
|
4561
|
+
existing.env.COZELOOP_API_BASE_URL = loopBaseUrl;
|
|
4562
|
+
delete existing.env.OTEL_ENDPOINT;
|
|
4563
|
+
} else if (otelEndpoint) {
|
|
4564
|
+
existing.env.OTEL_ENDPOINT = otelEndpoint;
|
|
4565
|
+
delete existing.env.COZELOOP_API_BASE_URL;
|
|
4536
4566
|
}
|
|
4537
4567
|
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4538
4568
|
existing.env.x_tt_env = PPE_TT_ENV;
|
|
@@ -4574,10 +4604,12 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
|
4574
4604
|
shellEnvLine('COZELOOP_WORKSPACE_ID', workspaceId),
|
|
4575
4605
|
];
|
|
4576
4606
|
if (cloud) {
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
envLines.push(shellEnvLine('
|
|
4607
|
+
const loopToken = readEnv('COZELOOP_API_TOKEN');
|
|
4608
|
+
const cozeToken = readEnv('COZE_API_TOKEN');
|
|
4609
|
+
if (loopToken) {
|
|
4610
|
+
envLines.push(shellEnvLine('COZELOOP_API_TOKEN', loopToken));
|
|
4611
|
+
} else if (cozeToken) {
|
|
4612
|
+
envLines.push(shellEnvLine('COZE_API_TOKEN', cozeToken));
|
|
4581
4613
|
}
|
|
4582
4614
|
} else {
|
|
4583
4615
|
envLines.push(shellEnvLine('COZELOOP_API_TOKEN', token));
|
|
@@ -4585,10 +4617,12 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
|
4585
4617
|
envLines.push(shellEnvLine('CODEX_HOME', home));
|
|
4586
4618
|
envLines.push(shellEnvLine('COZELOOP_HOOK_LOG', logFile));
|
|
4587
4619
|
envLines.push('TRACE_TO_COZELOOP=true');
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
envLines.push(shellEnvLine('
|
|
4620
|
+
const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
|
|
4621
|
+
const otelEndpoint = readEnv('OTEL_ENDPOINT');
|
|
4622
|
+
if (loopBaseUrl) {
|
|
4623
|
+
envLines.push(shellEnvLine('COZELOOP_API_BASE_URL', loopBaseUrl));
|
|
4624
|
+
} else if (otelEndpoint) {
|
|
4625
|
+
envLines.push(shellEnvLine('OTEL_ENDPOINT', otelEndpoint));
|
|
4592
4626
|
}
|
|
4593
4627
|
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4594
4628
|
envLines.push(shellEnvLine('x_tt_env', PPE_TT_ENV));
|
|
@@ -4875,6 +4909,105 @@ function httpsPost(url, body, extraHeaders) {
|
|
|
4875
4909
|
});
|
|
4876
4910
|
}
|
|
4877
4911
|
|
|
4912
|
+
function runCommand(input) {
|
|
4913
|
+
return new Promise((resolve) => {
|
|
4914
|
+
const { spawn } = require('child_process');
|
|
4915
|
+
const child = spawn(input.cmd, input.args || [], {
|
|
4916
|
+
env: input.env || process.env,
|
|
4917
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
4918
|
+
});
|
|
4919
|
+
let stdout = '';
|
|
4920
|
+
let stderr = '';
|
|
4921
|
+
let timedOut = false;
|
|
4922
|
+
const timer = setTimeout(() => {
|
|
4923
|
+
timedOut = true;
|
|
4924
|
+
child.kill('SIGKILL');
|
|
4925
|
+
}, input.timeoutMs || 30000);
|
|
4926
|
+
child.stdout.on('data', chunk => { stdout += chunk.toString(); });
|
|
4927
|
+
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
|
4928
|
+
child.on('close', code => {
|
|
4929
|
+
clearTimeout(timer);
|
|
4930
|
+
resolve({ code: timedOut ? -1 : code, stdout, stderr, timedOut });
|
|
4931
|
+
});
|
|
4932
|
+
child.stdin.end(input.stdin || '');
|
|
4933
|
+
});
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
async function verifyTraceReportViaSdk(token, workspaceId, pairCode, pythonCmd, tokenSource) {
|
|
4937
|
+
const pair = pairCode || crypto.randomBytes(6).toString('hex');
|
|
4938
|
+
const apiBase = getCozeloopApiBaseUrl(true);
|
|
4939
|
+
const script = `
|
|
4940
|
+
import json
|
|
4941
|
+
import os
|
|
4942
|
+
import sys
|
|
4943
|
+
|
|
4944
|
+
events = []
|
|
4945
|
+
|
|
4946
|
+
def finish_event(info):
|
|
4947
|
+
if getattr(info, "is_event_fail", False):
|
|
4948
|
+
events.append(getattr(info, "detail_msg", "") or "trace export failed")
|
|
4949
|
+
|
|
4950
|
+
try:
|
|
4951
|
+
import cozeloop
|
|
4952
|
+
kwargs = {
|
|
4953
|
+
"workspace_id": os.environ["COZELOOP_WORKSPACE_ID"],
|
|
4954
|
+
"api_token": os.environ["COZELOOP_API_TOKEN"],
|
|
4955
|
+
"upload_timeout": 30,
|
|
4956
|
+
"trace_finish_event_processor": finish_event,
|
|
4957
|
+
}
|
|
4958
|
+
api_base_url = os.environ.get("COZELOOP_API_BASE_URL", "").strip()
|
|
4959
|
+
if api_base_url:
|
|
4960
|
+
kwargs["api_base_url"] = api_base_url
|
|
4961
|
+
client = cozeloop.new_client(**kwargs)
|
|
4962
|
+
with client.start_span(name="cozelab-onboard-selfcheck", span_type="main", start_new_trace=True) as span:
|
|
4963
|
+
span.set_tags({"pair_code": os.environ.get("COZELOOP_PAIR_CODE", ""), "source": "cozelab-onboard"})
|
|
4964
|
+
span.set_input("cozelab-onboard selfcheck")
|
|
4965
|
+
span.set_output("ok")
|
|
4966
|
+
client.flush()
|
|
4967
|
+
client.close()
|
|
4968
|
+
if events:
|
|
4969
|
+
print(json.dumps({"success": False, "body": "\\n".join(events), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
|
|
4970
|
+
sys.exit(1)
|
|
4971
|
+
print(json.dumps({"success": True, "body": "", "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
|
|
4972
|
+
except Exception as e:
|
|
4973
|
+
print(json.dumps({"success": False, "body": str(e), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
|
|
4974
|
+
sys.exit(1)
|
|
4975
|
+
`;
|
|
4976
|
+
const env = {
|
|
4977
|
+
...process.env,
|
|
4978
|
+
COZELOOP_WORKSPACE_ID: workspaceId,
|
|
4979
|
+
COZELOOP_API_TOKEN: token,
|
|
4980
|
+
COZELOOP_PAIR_CODE: pair,
|
|
4981
|
+
COZELOOP_TOKEN_SOURCE: tokenSource || '',
|
|
4982
|
+
x_tt_env: PPE_TT_ENV,
|
|
4983
|
+
x_use_ppe: PPE_USE_PPE,
|
|
4984
|
+
};
|
|
4985
|
+
if (apiBase) env.COZELOOP_API_BASE_URL = apiBase;
|
|
4986
|
+
const result = await runCommand({
|
|
4987
|
+
cmd: pythonCmd || 'python3',
|
|
4988
|
+
args: ['-c', script],
|
|
4989
|
+
env,
|
|
4990
|
+
timeoutMs: 45000,
|
|
4991
|
+
});
|
|
4992
|
+
let parsed = null;
|
|
4993
|
+
const out = (result.stdout || '').trim().split(/\n/).filter(Boolean).pop();
|
|
4994
|
+
if (out) {
|
|
4995
|
+
try { parsed = JSON.parse(out); } catch { /* keep null */ }
|
|
4996
|
+
}
|
|
4997
|
+
const body = parsed?.body || result.stderr || result.stdout || (result.timedOut ? 'SDK selfcheck timed out' : '');
|
|
4998
|
+
const success = result.code === 0 && parsed?.success === true;
|
|
4999
|
+
if (success) {
|
|
5000
|
+
ok(`trace 上报成功 (pair_code=${pair})`);
|
|
5001
|
+
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
5002
|
+
} else {
|
|
5003
|
+
warn(`trace 上报失败: SDK selfcheck exit ${result.code}`);
|
|
5004
|
+
info(`trace selfcheck api_base_url=${apiBase || COZE_API}, token_source=${tokenSource || 'unknown'}`);
|
|
5005
|
+
const snippet = String(body || '').slice(0, 300);
|
|
5006
|
+
if (snippet) console.log(snippet);
|
|
5007
|
+
}
|
|
5008
|
+
return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource };
|
|
5009
|
+
}
|
|
5010
|
+
|
|
4878
5011
|
// 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
|
|
4879
5012
|
// 只看 HTTP 状态码(2xx=通),不回查 trace 是否落库——回查由外部查询方完成。
|
|
4880
5013
|
// pairCode 写进 span 的 pair_code attribute,供查询方按该字段过滤回查;缺省自动生成。
|
|
@@ -5287,25 +5420,37 @@ async function main() {
|
|
|
5287
5420
|
|
|
5288
5421
|
// Step 1: Authorize.
|
|
5289
5422
|
// 云端(--cloud):token 取自 sandbox 注入的环境变量,跳过 OAuth / credentials.json。
|
|
5290
|
-
//
|
|
5423
|
+
// trace 上报需要 COZELOOP_API_TOKEN;COZE_API_TOKEN 只保留为历史兼容来源,
|
|
5424
|
+
// onboard 会写入 hook,但自检会明确标记它不是 CozeLoop ingest token。
|
|
5291
5425
|
// 本地:load cached → refresh → device code。
|
|
5292
5426
|
// 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
|
|
5293
5427
|
let token;
|
|
5428
|
+
let tokenSource = '';
|
|
5429
|
+
let cloudTraceUsableToken = true;
|
|
5294
5430
|
if (args.cloud) {
|
|
5295
|
-
info('Step 1/5: 云端模式,从环境变量读取
|
|
5296
|
-
|
|
5431
|
+
info('Step 1/5: 云端模式,从环境变量读取 trace token...');
|
|
5432
|
+
const tokenInfo = getCloudTokenInfo();
|
|
5433
|
+
token = tokenInfo.token;
|
|
5434
|
+
tokenSource = tokenInfo.source;
|
|
5435
|
+
cloudTraceUsableToken = tokenInfo.traceUsable;
|
|
5297
5436
|
if (!token) {
|
|
5298
5437
|
errorBox([
|
|
5299
5438
|
'ERROR: --cloud 模式要求环境变量 COZELOOP_API_TOKEN',
|
|
5300
5439
|
'',
|
|
5301
|
-
'云端 sandbox 应在进程环境中注入 COZELOOP_API_TOKEN
|
|
5440
|
+
'云端 sandbox 应在进程环境中注入 COZELOOP_API_TOKEN。',
|
|
5302
5441
|
'未检测到该变量,无法配置 trace 上报。',
|
|
5303
5442
|
]);
|
|
5304
5443
|
}
|
|
5305
|
-
|
|
5444
|
+
cloudResult.token_source = tokenSource;
|
|
5445
|
+
if (cloudTraceUsableToken) {
|
|
5446
|
+
ok(`已从环境变量读取 ${tokenSource}`);
|
|
5447
|
+
} else {
|
|
5448
|
+
warn('仅检测到 COZE_API_TOKEN;它不是 CozeLoop SDK trace ingest token,自检预计会失败。');
|
|
5449
|
+
}
|
|
5306
5450
|
} else {
|
|
5307
5451
|
info('Step 1/5: 检查授权状态...');
|
|
5308
5452
|
token = await getValidToken();
|
|
5453
|
+
tokenSource = 'credentials';
|
|
5309
5454
|
}
|
|
5310
5455
|
console.log('');
|
|
5311
5456
|
|
|
@@ -5389,7 +5534,24 @@ async function main() {
|
|
|
5389
5534
|
|
|
5390
5535
|
// Step 5: Verify trace reporting end-to-end
|
|
5391
5536
|
info('Step 5/5: 验证 trace 上报链路...');
|
|
5392
|
-
|
|
5537
|
+
let verifyResult;
|
|
5538
|
+
if (args.cloud && !cloudTraceUsableToken) {
|
|
5539
|
+
verifyResult = {
|
|
5540
|
+
success: false,
|
|
5541
|
+
status: 0,
|
|
5542
|
+
body: '云端仅提供 COZE_API_TOKEN。Coze API token 不能作为 CozeLoop Python SDK 的 COZELOOP_API_TOKEN 上报 trace;请注入具备 CozeLoop trace ingest 权限的 COZELOOP_API_TOKEN。',
|
|
5543
|
+
traceId: '',
|
|
5544
|
+
pairCode: args.pairCode || '',
|
|
5545
|
+
tokenSource,
|
|
5546
|
+
apiBaseUrl: getCozeloopApiBaseUrl(true),
|
|
5547
|
+
};
|
|
5548
|
+
warn('trace 上报跳过:缺少 COZELOOP_API_TOKEN。');
|
|
5549
|
+
console.log(verifyResult.body);
|
|
5550
|
+
} else {
|
|
5551
|
+
verifyResult = args.cloud
|
|
5552
|
+
? await verifyTraceReportViaSdk(token, WORKSPACE_ID, args.pairCode, pythonCmd || 'python3', tokenSource)
|
|
5553
|
+
: await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
|
|
5554
|
+
}
|
|
5393
5555
|
if (verifyResult.success) {
|
|
5394
5556
|
cloudResult.verify = 'ok';
|
|
5395
5557
|
} else if (CLOUD_MODE) {
|
package/package.json
CHANGED
|
@@ -82,6 +82,7 @@ else:
|
|
|
82
82
|
DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
|
|
83
83
|
_COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
|
|
84
84
|
_COZE_API = "https://api.coze.cn"
|
|
85
|
+
_OTEL_SUFFIX = "/v1/loop/opentelemetry"
|
|
85
86
|
_REFRESH_THRESHOLD = 10 * 60 # refresh when < 10 minutes remain
|
|
86
87
|
_DEFAULT_WORKSPACE_ID = "7644910356078837760" # hardcoded spaceID fallback
|
|
87
88
|
|
|
@@ -247,8 +248,31 @@ def _refresh_token(refresh_token: str) -> Optional[str]:
|
|
|
247
248
|
debug_log(f"Token refresh failed: {e}")
|
|
248
249
|
return None
|
|
249
250
|
|
|
251
|
+
def _normalize_api_base_url(url: str) -> str:
|
|
252
|
+
base = (url or "").strip().rstrip("/")
|
|
253
|
+
if base.endswith(_OTEL_SUFFIX + "/v1/traces"):
|
|
254
|
+
return base[:-len(_OTEL_SUFFIX + "/v1/traces")].rstrip("/")
|
|
255
|
+
if base.endswith("/api/v1/loop/opentelemetry/v1/traces"):
|
|
256
|
+
return base[:-len("/v1/loop/opentelemetry/v1/traces")].rstrip("/")
|
|
257
|
+
if base.endswith(_OTEL_SUFFIX):
|
|
258
|
+
return base[:-len(_OTEL_SUFFIX)].rstrip("/")
|
|
259
|
+
if base.endswith("/api/v1/loop/opentelemetry"):
|
|
260
|
+
return base[:-len("/v1/loop/opentelemetry")].rstrip("/")
|
|
261
|
+
if base.endswith("/api/v1"):
|
|
262
|
+
return base[:-len("/v1")].rstrip("/")
|
|
263
|
+
return base
|
|
264
|
+
|
|
265
|
+
def get_api_base_url() -> str:
|
|
266
|
+
return _normalize_api_base_url(
|
|
267
|
+
os.environ.get("COZELOOP_API_BASE_URL", "") or os.environ.get("OTEL_ENDPOINT", "")
|
|
268
|
+
)
|
|
269
|
+
|
|
250
270
|
def get_fresh_token() -> Optional[str]:
|
|
251
271
|
"""Return a valid access token, refreshing if needed. Falls back to env var."""
|
|
272
|
+
env_token = os.environ.get("COZELOOP_API_TOKEN")
|
|
273
|
+
env_coze_token = os.environ.get("COZE_API_TOKEN")
|
|
274
|
+
if env_token:
|
|
275
|
+
return env_token
|
|
252
276
|
creds = _load_credentials()
|
|
253
277
|
if creds:
|
|
254
278
|
expires_at_sec = creds.get("expires_at", 0) / 1000
|
|
@@ -262,8 +286,9 @@ def get_fresh_token() -> Optional[str]:
|
|
|
262
286
|
if new_token:
|
|
263
287
|
return new_token
|
|
264
288
|
debug_log("Refresh failed, falling back to env var.")
|
|
265
|
-
|
|
266
|
-
|
|
289
|
+
if env_coze_token:
|
|
290
|
+
debug_log("COZE_API_TOKEN present but ignored; CozeLoop trace upload requires COZELOOP_API_TOKEN")
|
|
291
|
+
return None
|
|
267
292
|
|
|
268
293
|
# -------------------------------------------------------------------------
|
|
269
294
|
|
|
@@ -873,6 +898,9 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
873
898
|
client_kwargs["workspace_id"] = workspace_id
|
|
874
899
|
if token:
|
|
875
900
|
client_kwargs["api_token"] = token
|
|
901
|
+
api_base_url = get_api_base_url()
|
|
902
|
+
if api_base_url:
|
|
903
|
+
client_kwargs["api_base_url"] = api_base_url
|
|
876
904
|
client = cozeloop.new_client(**client_kwargs)
|
|
877
905
|
|
|
878
906
|
try:
|
|
@@ -1448,5 +1476,3 @@ def main():
|
|
|
1448
1476
|
|
|
1449
1477
|
if __name__ == "__main__":
|
|
1450
1478
|
main()
|
|
1451
|
-
|
|
1452
|
-
|
|
@@ -230,6 +230,10 @@ def get_api_base_url() -> str:
|
|
|
230
230
|
|
|
231
231
|
|
|
232
232
|
def get_fresh_token():
|
|
233
|
+
env_token = os.environ.get("COZELOOP_API_TOKEN")
|
|
234
|
+
env_coze_token = os.environ.get("COZE_API_TOKEN")
|
|
235
|
+
if env_token:
|
|
236
|
+
return env_token
|
|
233
237
|
creds = _load_credentials()
|
|
234
238
|
if creds:
|
|
235
239
|
remaining = creds.get("expires_at", 0) / 1000 - time.time()
|
|
@@ -239,13 +243,8 @@ def get_fresh_token():
|
|
|
239
243
|
new_token = _refresh_token(creds["refresh_token"])
|
|
240
244
|
if new_token:
|
|
241
245
|
return new_token
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return token
|
|
245
|
-
if get_api_base_url():
|
|
246
|
-
return os.environ.get("COZE_API_TOKEN")
|
|
247
|
-
if os.environ.get("COZE_API_TOKEN"):
|
|
248
|
-
hook_log("COZE_API_TOKEN present but ignored without COZELOOP_API_BASE_URL or OTEL_ENDPOINT")
|
|
246
|
+
if env_coze_token:
|
|
247
|
+
hook_log("COZE_API_TOKEN present but ignored; CozeLoop trace upload requires COZELOOP_API_TOKEN")
|
|
249
248
|
return None
|
|
250
249
|
# -------------------------------------------------------------------------
|
|
251
250
|
|