coze_lab 0.1.5 → 0.1.7
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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# coze_lab
|
|
2
2
|
|
|
3
3
|
Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop.
|
|
4
4
|
|
|
@@ -6,13 +6,13 @@ Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to Coz
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# First-time setup — triggers browser OAuth authorization
|
|
9
|
-
npx
|
|
9
|
+
npx coze_lab --agent=<type>
|
|
10
10
|
|
|
11
11
|
# Auth-only commands (no agent configuration)
|
|
12
|
-
npx
|
|
13
|
-
npx
|
|
14
|
-
npx
|
|
15
|
-
npx
|
|
12
|
+
npx coze_lab --login # Device Code login only
|
|
13
|
+
npx coze_lab --status # Show current authorization status
|
|
14
|
+
npx coze_lab --refresh # Force-refresh the access token
|
|
15
|
+
npx coze_lab --logout # Clear cached credentials
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
### Parameters
|
|
@@ -62,7 +62,7 @@ OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
|
|
|
62
62
|
|
|
63
63
|
This means traces continue to upload without interruption even across token expiry, as long as the refresh token is still valid.
|
|
64
64
|
|
|
65
|
-
**For OpenClaw**, the token is read from `openclaw.json` at startup. Re-run `npx
|
|
65
|
+
**For OpenClaw**, the token is read from `openclaw.json` at startup. Re-run `npx coze_lab --agent=openclaw` to refresh it if OpenClaw reports auth errors.
|
|
66
66
|
|
|
67
67
|
## Supported versions
|
|
68
68
|
|
package/index.js
CHANGED
|
@@ -12,6 +12,33 @@ const CREDS_PATH = require('path').join(require('os').homedir(), '.cozeloop', '
|
|
|
12
12
|
// Refresh when less than 10 minutes remain
|
|
13
13
|
const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
|
|
14
14
|
|
|
15
|
+
// ─── 1. Cloud structured output ──────────────────────────────────────────────
|
|
16
|
+
// 云端(--cloud)模式:在 stdout 输出一行机器可读结果 COZE_LAB_RESULT={...},
|
|
17
|
+
// 供管理后台解析判定(inject/verify/logid/message),不依赖中文文案。
|
|
18
|
+
let CLOUD_MODE = false;
|
|
19
|
+
const cloudResult = { inject: 'skip', verify: 'skip', logid: '', message: '' };
|
|
20
|
+
|
|
21
|
+
// errorBox 在云端模式下抛此异常(而非 process.exit),由 main 外层统一收尾。
|
|
22
|
+
class CloudAbort extends Error {}
|
|
23
|
+
|
|
24
|
+
// 从 onboard 失败输出 / verify response body 中提取 server logid。
|
|
25
|
+
// 兼容两种形态:JSON 里的 "logid":"xxx" / detail.logid,以及裸 logid=xxx。
|
|
26
|
+
function extractLogid(text) {
|
|
27
|
+
if (!text) return '';
|
|
28
|
+
let m = text.match(/"logid"\s*:\s*"([a-zA-Z0-9]+)"/);
|
|
29
|
+
if (m) return m[1];
|
|
30
|
+
m = text.match(/logid[=:\s]+([a-zA-Z0-9]+)/i);
|
|
31
|
+
return m ? m[1] : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 输出结构化结果行(仅云端模式)。
|
|
35
|
+
function emitCloudResult() {
|
|
36
|
+
if (!CLOUD_MODE) return;
|
|
37
|
+
// message 压成单行,避免破坏 COZE_LAB_RESULT 单行约定。
|
|
38
|
+
const safe = { ...cloudResult, message: String(cloudResult.message || '').replace(/\s+/g, ' ').slice(0, 500) };
|
|
39
|
+
console.log('COZE_LAB_RESULT=' + JSON.stringify(safe));
|
|
40
|
+
}
|
|
41
|
+
|
|
15
42
|
// ─── 1. Color helpers ────────────────────────────────────────────────────────
|
|
16
43
|
const C = {
|
|
17
44
|
reset: '\x1b[0m',
|
|
@@ -41,6 +68,10 @@ function box(lines, color) {
|
|
|
41
68
|
|
|
42
69
|
function errorBox(lines) {
|
|
43
70
|
box(lines, C.red);
|
|
71
|
+
// 云端模式下不直接退出,改抛异常,让 main 外层统一输出 COZE_LAB_RESULT 结构化结果。
|
|
72
|
+
if (CLOUD_MODE) {
|
|
73
|
+
throw new CloudAbort(lines.join(' '));
|
|
74
|
+
}
|
|
44
75
|
process.exit(1);
|
|
45
76
|
}
|
|
46
77
|
|
|
@@ -4114,6 +4145,8 @@ function parseArgs() {
|
|
|
4114
4145
|
if (arg === '--login') { args['login'] = true; continue; }
|
|
4115
4146
|
if (arg === '--status') { args['status'] = true; continue; }
|
|
4116
4147
|
if (arg === '--refresh') { args['refresh'] = true; continue; }
|
|
4148
|
+
if (arg === '--verify') { args['verify'] = true; continue; }
|
|
4149
|
+
if (arg === '--cloud') { args['cloud'] = true; continue; }
|
|
4117
4150
|
const m = arg.match(/^--([^=]+)=(.+)$/);
|
|
4118
4151
|
if (m) args[m[1]] = m[2];
|
|
4119
4152
|
}
|
|
@@ -4157,8 +4190,31 @@ function validateArgs(args) {
|
|
|
4157
4190
|
if (args['login']) return { login: true };
|
|
4158
4191
|
if (args['status']) return { status: true };
|
|
4159
4192
|
if (args['refresh']) return { refresh: true };
|
|
4193
|
+
if (args['verify']) return { verify: true, pairCode: args['pair-code'] };
|
|
4194
|
+
|
|
4195
|
+
// --cloud + --agent-id:云端 sandbox 没有本地 coze-bridge 的 ~/.coze/agents/<id>/config.json,
|
|
4196
|
+
// framework 必须由命令行 --agent 显式传(前端拼命令时已知 agent.framework)。不读本地 config。
|
|
4197
|
+
if (args['cloud'] && args['agent-id']) {
|
|
4198
|
+
if (!args['agent'] || !VALID_AGENTS.includes(args['agent'])) {
|
|
4199
|
+
errorBox([
|
|
4200
|
+
'ERROR: --cloud --agent-id=<id> 必须同时显式指定 --agent=<type>',
|
|
4201
|
+
'',
|
|
4202
|
+
'云端 sandbox 无本地 config.json,无法自动推断 framework。',
|
|
4203
|
+
`请拼成:npx coze_lab --cloud --agent-id=${args['agent-id']} --agent=claude-code|codex|openclaw`,
|
|
4204
|
+
]);
|
|
4205
|
+
}
|
|
4206
|
+
return {
|
|
4207
|
+
agent: args['agent'],
|
|
4208
|
+
agentId: args['agent-id'],
|
|
4209
|
+
// 云端 claude-code 配置写进 agent workspace,缺省按约定路径推断(见 main)。
|
|
4210
|
+
workspace: args['workspace'] || '',
|
|
4211
|
+
'codex-home': args['codex-home'],
|
|
4212
|
+
pairCode: args['pair-code'],
|
|
4213
|
+
cloud: true,
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4160
4216
|
|
|
4161
|
-
//
|
|
4217
|
+
// 本地 --agent-id:读 ~/.coze/agents/<id>/config.json 的 framework 自动路由。
|
|
4162
4218
|
if (args['agent-id']) {
|
|
4163
4219
|
const resolved = resolveAgent(args['agent-id']);
|
|
4164
4220
|
return {
|
|
@@ -4167,6 +4223,8 @@ function validateArgs(args) {
|
|
|
4167
4223
|
workspace: resolved.workspace,
|
|
4168
4224
|
agentRoot: resolved.root,
|
|
4169
4225
|
'codex-home': args['codex-home'],
|
|
4226
|
+
pairCode: args['pair-code'],
|
|
4227
|
+
cloud: !!args['cloud'],
|
|
4170
4228
|
};
|
|
4171
4229
|
}
|
|
4172
4230
|
|
|
@@ -4183,6 +4241,8 @@ function validateArgs(args) {
|
|
|
4183
4241
|
' --login Login (Device Code flow)',
|
|
4184
4242
|
' --refresh Force refresh access token',
|
|
4185
4243
|
' --logout Clear cached credentials',
|
|
4244
|
+
' --verify Send a test trace to verify the reporting pipeline',
|
|
4245
|
+
' 可带 --pair-code=<值> 写入 trace metadata(缺省自动生成),供查询方回查',
|
|
4186
4246
|
]);
|
|
4187
4247
|
}
|
|
4188
4248
|
if (!VALID_AGENTS.includes(args['agent'])) {
|
|
@@ -4195,7 +4255,7 @@ function validateArgs(args) {
|
|
|
4195
4255
|
' --agent=openclaw',
|
|
4196
4256
|
]);
|
|
4197
4257
|
}
|
|
4198
|
-
return { agent: args['agent'], 'codex-home': args['codex-home'] };
|
|
4258
|
+
return { agent: args['agent'], 'codex-home': args['codex-home'], pairCode: args['pair-code'], cloud: !!args['cloud'] };
|
|
4199
4259
|
}
|
|
4200
4260
|
|
|
4201
4261
|
// ─── 4. Agent detection ──────────────────────────────────────────────────────
|
|
@@ -4341,7 +4401,7 @@ function checkVersionWhitelist(agent, version) {
|
|
|
4341
4401
|
'Trace reporting may not work correctly.',
|
|
4342
4402
|
'Please upgrade and re-run:',
|
|
4343
4403
|
` ${UPGRADE_CMD[agent]}`,
|
|
4344
|
-
` npx
|
|
4404
|
+
` npx coze_lab --agent=${agent} ...`,
|
|
4345
4405
|
]);
|
|
4346
4406
|
}
|
|
4347
4407
|
|
|
@@ -4386,7 +4446,9 @@ function mergeJson(filepath, mergeFn) {
|
|
|
4386
4446
|
// writeClaudeCodeHook 配置 Claude Code 的 hook。
|
|
4387
4447
|
// configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
|
|
4388
4448
|
// settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
|
|
4389
|
-
|
|
4449
|
+
// cloud=true 时不把明文 token 写进 settings.local.json —— 云端 hook 运行时直接读
|
|
4450
|
+
// 环境变量 COZE_API_TOKEN(见 scripts/claude-code/cozeloop_hook.py 的 get_fresh_token)。
|
|
4451
|
+
function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cloud) {
|
|
4390
4452
|
const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
|
|
4391
4453
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
4392
4454
|
const baseDir = configBaseDir || process.cwd();
|
|
@@ -4439,7 +4501,13 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir) {
|
|
|
4439
4501
|
const localSettings = mergeJson(localSettingsPath, (existing) => {
|
|
4440
4502
|
if (!existing.env) existing.env = {};
|
|
4441
4503
|
existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
|
|
4442
|
-
|
|
4504
|
+
// 云端:token 由 sandbox 注入到环境变量 COZE_API_TOKEN,hook 运行时直接读,
|
|
4505
|
+
// 不在配置文件落明文 token。本地:写入 OAuth 拿到的 token。
|
|
4506
|
+
if (cloud) {
|
|
4507
|
+
delete existing.env.COZELOOP_API_TOKEN;
|
|
4508
|
+
} else {
|
|
4509
|
+
existing.env.COZELOOP_API_TOKEN = patToken;
|
|
4510
|
+
}
|
|
4443
4511
|
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4444
4512
|
existing.env.x_tt_env = PPE_TT_ENV;
|
|
4445
4513
|
existing.env.x_use_ppe = PPE_USE_PPE;
|
|
@@ -4458,7 +4526,9 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir) {
|
|
|
4458
4526
|
|
|
4459
4527
|
// writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
|
|
4460
4528
|
// 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
|
|
4461
|
-
|
|
4529
|
+
// cloud=true 时 cozeloop.env 不写死 token —— 云端 hook 运行时直接读环境变量
|
|
4530
|
+
// COZE_API_TOKEN(见 scripts/codex/cozeloop_hook.py 的 get_fresh_token)。
|
|
4531
|
+
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
4462
4532
|
const home = codexHome || path.join(os.homedir(), '.codex');
|
|
4463
4533
|
const hooksDir = path.join(home, 'hooks');
|
|
4464
4534
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
@@ -4473,14 +4543,18 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome) {
|
|
|
4473
4543
|
writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
|
|
4474
4544
|
|
|
4475
4545
|
// 2. Write env file with chmod 600
|
|
4476
|
-
|
|
4546
|
+
// 云端(cloud):不落明文 token,hook 运行时从环境变量 COZE_API_TOKEN 读取。
|
|
4547
|
+
const envLines = [
|
|
4477
4548
|
`COZELOOP_WORKSPACE_ID=${workspaceId}`,
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4549
|
+
];
|
|
4550
|
+
if (!cloud) {
|
|
4551
|
+
envLines.push(`COZELOOP_API_TOKEN=${token}`);
|
|
4552
|
+
}
|
|
4553
|
+
envLines.push('TRACE_TO_COZELOOP=true');
|
|
4554
|
+
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4555
|
+
envLines.push(`x_tt_env=${PPE_TT_ENV}`);
|
|
4556
|
+
envLines.push(`x_use_ppe=${PPE_USE_PPE}`);
|
|
4557
|
+
const envContent = envLines.join('\n') + '\n';
|
|
4484
4558
|
try {
|
|
4485
4559
|
fs.writeFileSync(envFile, envContent, { mode: 0o600 });
|
|
4486
4560
|
} catch (e) {
|
|
@@ -4518,12 +4592,16 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome) {
|
|
|
4518
4592
|
errorBox([`ERROR: Cannot write ${hooksJson}`, '', e.message]);
|
|
4519
4593
|
}
|
|
4520
4594
|
ok(`Hook registered in ${hooksJson}`);
|
|
4521
|
-
warn('Codex hook trust:
|
|
4595
|
+
warn('Codex hook trust: 首次启动 Codex 会提示 "Hooks need review"。在该提示按 t(Trust all and continue),或在会话内运行 /hooks 后按 t,即可一次性信任全部 hook 启用 trace 上报。');
|
|
4522
4596
|
|
|
4523
4597
|
return { hookScript, envFile, hooksJson };
|
|
4524
4598
|
}
|
|
4525
4599
|
|
|
4526
|
-
|
|
4600
|
+
// writeOpenClawHook 配置 OpenClaw 的 cozeloop-trace 插件(全局装在 ~/.openclaw)。
|
|
4601
|
+
// agentId 非空时并入 plugins.entries[...].config.traceAgentIds allowlist —— 插件运行时
|
|
4602
|
+
// 用 resolveAgentIdFromHookCtx 取当前 agentId,仅 allowlist 内的 agent 才上报 trace。
|
|
4603
|
+
// allowlist 为空(本地全局模式)= 全部放行,向后兼容。
|
|
4604
|
+
function writeOpenClawHook(token, workspaceId, agentId) {
|
|
4527
4605
|
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
4528
4606
|
const pluginDir = path.join(os.homedir(), '.cozeloop', 'openclaw-plugin');
|
|
4529
4607
|
|
|
@@ -4601,12 +4679,25 @@ function writeOpenClawHook(token, workspaceId) {
|
|
|
4601
4679
|
existing.plugins.entries[PLUGIN].enabled = true;
|
|
4602
4680
|
// hooks.allowConversationAccess required for 2026.5+ to access session content
|
|
4603
4681
|
existing.plugins.entries[PLUGIN].hooks = { allowConversationAccess: true };
|
|
4604
|
-
existing.plugins.entries[PLUGIN].config = {
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4682
|
+
if (!existing.plugins.entries[PLUGIN].config) existing.plugins.entries[PLUGIN].config = {};
|
|
4683
|
+
const pcfg = existing.plugins.entries[PLUGIN].config;
|
|
4684
|
+
pcfg.authorization = `Bearer ${token}`;
|
|
4685
|
+
pcfg.endpoint = 'https://api.coze.cn/v1/loop/opentelemetry';
|
|
4686
|
+
pcfg.workspaceId = workspaceId;
|
|
4687
|
+
pcfg.debug = true;
|
|
4688
|
+
// per-agent trace 放行:把当前 agentId 并入 traceAgentIds(去重、归一为小写,
|
|
4689
|
+
// 与插件侧 resolveAgentIdFromHookCtx 的归一一致)。无 agentId(全局模式)则不动
|
|
4690
|
+
// allowlist —— 空 allowlist 表示全部放行。
|
|
4691
|
+
if (agentId) {
|
|
4692
|
+
const norm = String(agentId).trim().toLowerCase();
|
|
4693
|
+
// 读 existing 时一并归一(小写、去空),与插件侧 resolveAgentIdFromHookCtx 一致,
|
|
4694
|
+
// 防手工编辑混入大写条目导致去重失效。
|
|
4695
|
+
const list = (Array.isArray(pcfg.traceAgentIds) ? pcfg.traceAgentIds : [])
|
|
4696
|
+
.map((s) => String(s).trim().toLowerCase())
|
|
4697
|
+
.filter(Boolean);
|
|
4698
|
+
if (norm && !list.includes(norm)) list.push(norm);
|
|
4699
|
+
pcfg.traceAgentIds = list;
|
|
4700
|
+
}
|
|
4610
4701
|
return existing;
|
|
4611
4702
|
});
|
|
4612
4703
|
|
|
@@ -4632,15 +4723,17 @@ function writeOpenClawHook(token, workspaceId) {
|
|
|
4632
4723
|
|
|
4633
4724
|
// ─── 8. Auth — Device Code OAuth + token store ───────────────────────────────
|
|
4634
4725
|
const https = require('https');
|
|
4726
|
+
const crypto = require('crypto');
|
|
4635
4727
|
|
|
4636
|
-
function httpsPost(url, body) {
|
|
4728
|
+
function httpsPost(url, body, extraHeaders) {
|
|
4637
4729
|
return new Promise((resolve, reject) => {
|
|
4638
4730
|
const data = JSON.stringify(body);
|
|
4639
4731
|
const u = new URL(url);
|
|
4640
4732
|
const req = https.request(
|
|
4641
4733
|
{ hostname: u.hostname, path: u.pathname + u.search, method: 'POST',
|
|
4642
4734
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data),
|
|
4643
|
-
'x-tt-env': PPE_TT_ENV, 'x-use-ppe': PPE_USE_PPE
|
|
4735
|
+
'x-tt-env': PPE_TT_ENV, 'x-use-ppe': PPE_USE_PPE,
|
|
4736
|
+
...(extraHeaders || {}) } },
|
|
4644
4737
|
(res) => {
|
|
4645
4738
|
let buf = '';
|
|
4646
4739
|
res.on('data', c => buf += c);
|
|
@@ -4654,6 +4747,66 @@ function httpsPost(url, body) {
|
|
|
4654
4747
|
});
|
|
4655
4748
|
}
|
|
4656
4749
|
|
|
4750
|
+
// 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
|
|
4751
|
+
// 只看 HTTP 状态码(2xx=通),不回查 trace 是否落库——回查由外部查询方完成。
|
|
4752
|
+
// pairCode 写进 span 的 pair_code attribute,供查询方按该字段过滤回查;缺省自动生成。
|
|
4753
|
+
// 不在函数内退出,退出行为交给调用方(主流程 Step 6 / 独立命令 --verify)。
|
|
4754
|
+
async function verifyTraceReport(token, workspaceId, pairCode) {
|
|
4755
|
+
const traceId = crypto.randomBytes(16).toString('hex'); // 32 hex chars
|
|
4756
|
+
const spanId = crypto.randomBytes(8).toString('hex'); // 16 hex chars
|
|
4757
|
+
const nowNs = String(Date.now() * 1_000_000); // OTLP 要求纳秒 unix 时间(字符串)
|
|
4758
|
+
// 缺省自动生成 12 位 hex 配对码;查询方按 field_name="pair_code" 回查。
|
|
4759
|
+
const pair = pairCode || crypto.randomBytes(6).toString('hex');
|
|
4760
|
+
|
|
4761
|
+
const otlpBody = {
|
|
4762
|
+
resourceSpans: [{
|
|
4763
|
+
resource: {
|
|
4764
|
+
attributes: [
|
|
4765
|
+
{ key: 'service.name', value: { stringValue: 'cozelab-onboard' } },
|
|
4766
|
+
],
|
|
4767
|
+
},
|
|
4768
|
+
scopeSpans: [{
|
|
4769
|
+
scope: { name: 'cozelab-onboard-selfcheck' },
|
|
4770
|
+
spans: [{
|
|
4771
|
+
traceId,
|
|
4772
|
+
spanId,
|
|
4773
|
+
name: 'cozelab-onboard-selfcheck',
|
|
4774
|
+
kind: 1,
|
|
4775
|
+
startTimeUnixNano: nowNs,
|
|
4776
|
+
endTimeUnixNano: nowNs,
|
|
4777
|
+
status: { code: 1 },
|
|
4778
|
+
attributes: [
|
|
4779
|
+
{ key: 'pair_code', value: { stringValue: pair } },
|
|
4780
|
+
],
|
|
4781
|
+
}],
|
|
4782
|
+
}],
|
|
4783
|
+
}],
|
|
4784
|
+
};
|
|
4785
|
+
|
|
4786
|
+
let res;
|
|
4787
|
+
try {
|
|
4788
|
+
res = await httpsPost(
|
|
4789
|
+
`${COZE_API}/v1/loop/opentelemetry/v1/traces`,
|
|
4790
|
+
otlpBody,
|
|
4791
|
+
{ Authorization: `Bearer ${token}`, 'cozeloop-workspace-id': workspaceId },
|
|
4792
|
+
);
|
|
4793
|
+
} catch (e) {
|
|
4794
|
+
warn(`trace 上报失败: ${e.message}`);
|
|
4795
|
+
return { success: false, status: 0, body: e.message, traceId, pairCode: pair };
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
const success = res.status >= 200 && res.status < 300;
|
|
4799
|
+
if (success) {
|
|
4800
|
+
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
|
|
4801
|
+
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
4802
|
+
} else {
|
|
4803
|
+
warn(`trace 上报失败: HTTP ${res.status}`);
|
|
4804
|
+
const snippet = (res.body || '').slice(0, 300);
|
|
4805
|
+
if (snippet) console.log(snippet);
|
|
4806
|
+
}
|
|
4807
|
+
return { success, status: res.status, body: res.body, traceId, pairCode: pair };
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4657
4810
|
function httpsGet(url, headers) {
|
|
4658
4811
|
return new Promise((resolve, reject) => {
|
|
4659
4812
|
// 合并 PPE 泳道 header
|
|
@@ -4911,21 +5064,10 @@ function authStatus() {
|
|
|
4911
5064
|
box(lines, statusColor);
|
|
4912
5065
|
}
|
|
4913
5066
|
|
|
4914
|
-
// ── Verify token is usable ────────────────────────────────────────────────
|
|
4915
|
-
async function verifyWorkspace(token) {
|
|
4916
|
-
info('验证 Token 有效性...');
|
|
4917
|
-
// Token validity is guaranteed by the Device Code OAuth flow.
|
|
4918
|
-
// We skip a separate API call here since the OAuth app may not have
|
|
4919
|
-
// Account.listWorkspace permission — the token itself is the proof.
|
|
4920
|
-
if (!token) return null;
|
|
4921
|
-
ok(`Token 有效,工作空间 ${WORKSPACE_ID} 确认。`);
|
|
4922
|
-
return token;
|
|
4923
|
-
}
|
|
4924
|
-
|
|
4925
5067
|
// ─── 9. Main ─────────────────────────────────────────────────────────────────
|
|
4926
5068
|
const NEXT_STEP = {
|
|
4927
|
-
'claude-code': 'Hook
|
|
4928
|
-
'codex': 'Hook
|
|
5069
|
+
'claude-code': 'Hook 已写入。Claude Code 会自动热重载 hooks,当前会话即刻生效,无需 /new 或重启。',
|
|
5070
|
+
'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex 会话(已配置 SessionStart hook,新会话自动加载)。',
|
|
4929
5071
|
'openclaw': 'OpenClaw gateway 已自动重启,trace 即刻生效。',
|
|
4930
5072
|
};
|
|
4931
5073
|
|
|
@@ -4991,41 +5133,58 @@ async function main() {
|
|
|
4991
5133
|
return;
|
|
4992
5134
|
}
|
|
4993
5135
|
|
|
5136
|
+
if (args.verify) {
|
|
5137
|
+
info('验证 trace 上报链路...');
|
|
5138
|
+
const token = await getValidToken(); // 无凭证会自动走登录/刷新
|
|
5139
|
+
console.log('');
|
|
5140
|
+
const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode);
|
|
5141
|
+
process.exit(result.success ? 0 : 1);
|
|
5142
|
+
}
|
|
5143
|
+
|
|
4994
5144
|
const { agent } = args;
|
|
5145
|
+
// 云端模式:开启结构化输出 + errorBox 抛异常(而非 exit)。
|
|
5146
|
+
CLOUD_MODE = !!args.cloud;
|
|
5147
|
+
// per-agent 路由时 agent 的 workspace(claude-code 用它做配置根目录)。
|
|
4995
5148
|
// per-agent 路由时 agent 的 workspace(claude-code 用它做配置根目录)。
|
|
4996
|
-
|
|
5149
|
+
// 云端 claude-code 未显式传 workspace 时,按约定路径 ~/.coze/agents/<id>/workspace 推断。
|
|
5150
|
+
let agentWorkspace = args.workspace || '';
|
|
5151
|
+
if (args.cloud && args.agentId && agent === 'claude-code' && !agentWorkspace) {
|
|
5152
|
+
agentWorkspace = path.join(os.homedir(), '.coze', 'agents', args.agentId, 'workspace');
|
|
5153
|
+
}
|
|
4997
5154
|
if (args.agentId) {
|
|
4998
5155
|
info(`目标 agent: ${args.agentId} (framework=${agent}, workspace=${agentWorkspace || 'N/A'})`);
|
|
4999
5156
|
console.log('');
|
|
5000
5157
|
}
|
|
5001
5158
|
|
|
5002
|
-
// Step 1: Authorize
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
if (verified === null) {
|
|
5017
|
-
errorBox(['ERROR: 重新授权后 Token 仍无效,请联系管理员。']);
|
|
5159
|
+
// Step 1: Authorize.
|
|
5160
|
+
// 云端(--cloud):token 取自环境变量 COZE_API_TOKEN,跳过 OAuth / credentials.json。
|
|
5161
|
+
// 本地:load cached → refresh → device code。
|
|
5162
|
+
let token;
|
|
5163
|
+
if (args.cloud) {
|
|
5164
|
+
info('Step 1/5: 云端模式,从环境变量 COZE_API_TOKEN 读取 token...');
|
|
5165
|
+
token = process.env.COZE_API_TOKEN;
|
|
5166
|
+
if (!token) {
|
|
5167
|
+
errorBox([
|
|
5168
|
+
'ERROR: --cloud 模式要求环境变量 COZE_API_TOKEN',
|
|
5169
|
+
'',
|
|
5170
|
+
'云端 sandbox 应在进程环境中注入 COZE_API_TOKEN。',
|
|
5171
|
+
'未检测到该变量,无法配置 trace 上报。',
|
|
5172
|
+
]);
|
|
5018
5173
|
}
|
|
5174
|
+
ok('已从环境变量读取 COZE_API_TOKEN');
|
|
5175
|
+
} else {
|
|
5176
|
+
info('Step 1/5: 检查授权状态...');
|
|
5177
|
+
token = await getValidToken();
|
|
5019
5178
|
}
|
|
5020
5179
|
console.log('');
|
|
5021
5180
|
|
|
5022
|
-
// Step
|
|
5023
|
-
info('Step
|
|
5181
|
+
// Step 2: Detect agent binary
|
|
5182
|
+
info('Step 2/5: Detecting agent...');
|
|
5024
5183
|
const version = detectAgent(agent);
|
|
5025
5184
|
console.log('');
|
|
5026
5185
|
|
|
5027
|
-
// Step
|
|
5028
|
-
info('Step
|
|
5186
|
+
// Step 3: Environment checks
|
|
5187
|
+
info('Step 3/5: Checking environment...');
|
|
5029
5188
|
let pythonCmd = null;
|
|
5030
5189
|
if (agent !== 'openclaw') {
|
|
5031
5190
|
pythonCmd = checkPython();
|
|
@@ -5034,20 +5193,33 @@ async function main() {
|
|
|
5034
5193
|
checkVersionWhitelist(agent, version);
|
|
5035
5194
|
console.log('');
|
|
5036
5195
|
|
|
5037
|
-
// Step
|
|
5038
|
-
info('Step
|
|
5196
|
+
// Step 4: Write hook configuration
|
|
5197
|
+
info('Step 4/5: Writing hook configuration...');
|
|
5039
5198
|
let written = {};
|
|
5040
5199
|
if (agent === 'claude-code') {
|
|
5041
5200
|
// per-agent 路由时把配置写进 agent 的 workspace(仅该 agent 生效)。
|
|
5042
|
-
|
|
5201
|
+
// 云端按 agentId 路由但 workspace 为空会导致 settings.json 落 ~/.claude、
|
|
5202
|
+
// settings.local.json 落进程 cwd —— per-agent 隔离失效。此时显式报错。
|
|
5203
|
+
if (args.agentId && !agentWorkspace) {
|
|
5204
|
+
errorBox([
|
|
5205
|
+
`ERROR: agent "${args.agentId}" 的 config.json 缺少 workspace 字段`,
|
|
5206
|
+
'',
|
|
5207
|
+
'claude-code per-agent 配置需要明确的 workspace 目录,',
|
|
5208
|
+
'否则 hook 配置会落到全局/进程目录,无法做到 per-agent 隔离。',
|
|
5209
|
+
]);
|
|
5210
|
+
}
|
|
5211
|
+
written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud);
|
|
5043
5212
|
} else if (agent === 'codex') {
|
|
5044
5213
|
// CODEX_HOME 来源优先级:--codex-home= > 环境变量 CODEX_HOME > ~/.codex(缺省)。
|
|
5045
5214
|
const codexHome = args['codex-home'] || process.env.CODEX_HOME || undefined;
|
|
5046
5215
|
if (codexHome) info(`Codex 配置目录: ${codexHome}`);
|
|
5047
|
-
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome);
|
|
5216
|
+
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
|
|
5048
5217
|
} else {
|
|
5049
|
-
|
|
5218
|
+
// openclaw:云端用 traceAgentIds allowlist 做 per-agent 放行。
|
|
5219
|
+
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId) || {};
|
|
5050
5220
|
}
|
|
5221
|
+
// 走到这里说明 detectAgent / 环境检查 / 写 hook 配置全部成功 → 注入成功。
|
|
5222
|
+
cloudResult.inject = 'ok';
|
|
5051
5223
|
console.log('');
|
|
5052
5224
|
|
|
5053
5225
|
// Success summary(用实际写入路径,per-agent / 自定义 CODEX_HOME 时也准确)
|
|
@@ -5079,9 +5251,52 @@ async function main() {
|
|
|
5079
5251
|
info('确保 CozeLoop Token 具备 Trace ingest 权限,否则 hook 触发但上报会失败。');
|
|
5080
5252
|
console.log('');
|
|
5081
5253
|
|
|
5254
|
+
// Step 5: Verify trace reporting end-to-end
|
|
5255
|
+
info('Step 5/5: 验证 trace 上报链路...');
|
|
5256
|
+
const verifyResult = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode);
|
|
5257
|
+
if (verifyResult.success) {
|
|
5258
|
+
cloudResult.verify = 'ok';
|
|
5259
|
+
} else if (CLOUD_MODE) {
|
|
5260
|
+
// 云端:注入已成功,验证失败不阻断(放行),记录结果供后台弹 warning。
|
|
5261
|
+
cloudResult.verify = 'fail';
|
|
5262
|
+
cloudResult.logid = extractLogid(verifyResult.body) || cloudResult.logid;
|
|
5263
|
+
cloudResult.message = `trace 上报自检失败 HTTP ${verifyResult.status}: ${(verifyResult.body || '').slice(0, 200)}`;
|
|
5264
|
+
warn('trace 上报自检失败,但 hook 配置已写入(云端放行)。');
|
|
5265
|
+
console.log('');
|
|
5266
|
+
emitCloudResult();
|
|
5267
|
+
successBox(summaryLines);
|
|
5268
|
+
return;
|
|
5269
|
+
} else {
|
|
5270
|
+
errorBox([
|
|
5271
|
+
'ERROR: trace 上报自检失败',
|
|
5272
|
+
'',
|
|
5273
|
+
`HTTP ${verifyResult.status}`,
|
|
5274
|
+
(verifyResult.body || '').slice(0, 300),
|
|
5275
|
+
'',
|
|
5276
|
+
'hook 配置已写入,但上报链路未打通。',
|
|
5277
|
+
'请确认 CozeLoop Token 具备 Trace ingest 权限,再重试。',
|
|
5278
|
+
]); // errorBox 内部 process.exit(1)
|
|
5279
|
+
}
|
|
5280
|
+
console.log('');
|
|
5281
|
+
|
|
5282
|
+
emitCloudResult();
|
|
5082
5283
|
successBox(summaryLines);
|
|
5083
5284
|
}
|
|
5084
5285
|
|
|
5085
5286
|
main().catch(e => {
|
|
5287
|
+
// 云端模式:失败时输出结构化结果(含 logid),exit 非 0 但带 COZE_LAB_RESULT 行。
|
|
5288
|
+
if (CLOUD_MODE) {
|
|
5289
|
+
if (cloudResult.inject !== 'ok') {
|
|
5290
|
+
// 写 hook 配置前的任何失败 → 注入失败。
|
|
5291
|
+
cloudResult.inject = 'fail';
|
|
5292
|
+
} else if (cloudResult.verify === 'skip') {
|
|
5293
|
+
// 注入已成功,但验证阶段异常崩溃(未走到正常的 ok/fail 判定)→ 记为验证失败。
|
|
5294
|
+
cloudResult.verify = 'fail';
|
|
5295
|
+
}
|
|
5296
|
+
if (!cloudResult.message) cloudResult.message = e && e.message ? e.message : 'unexpected failure';
|
|
5297
|
+
if (!cloudResult.logid) cloudResult.logid = extractLogid(cloudResult.message);
|
|
5298
|
+
emitCloudResult();
|
|
5299
|
+
process.exit(1);
|
|
5300
|
+
}
|
|
5086
5301
|
errorBox(['ERROR: Unexpected failure', '', e.message]);
|
|
5087
5302
|
});
|
package/package.json
CHANGED
|
@@ -45,7 +45,7 @@ DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
|
|
|
45
45
|
_COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
|
|
46
46
|
_COZE_API = "https://api.coze.cn"
|
|
47
47
|
_REFRESH_THRESHOLD = 10 * 60 # refresh when < 10 minutes remain
|
|
48
|
-
_DEFAULT_WORKSPACE_ID = "
|
|
48
|
+
_DEFAULT_WORKSPACE_ID = "7644910356078837760" # hardcoded spaceID fallback
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
# --- coze-context parsing -------------------------------------------------
|
|
@@ -224,7 +224,8 @@ def get_fresh_token() -> Optional[str]:
|
|
|
224
224
|
if new_token:
|
|
225
225
|
return new_token
|
|
226
226
|
debug_log("Refresh failed, falling back to env var.")
|
|
227
|
-
|
|
227
|
+
# Cloud sandbox: token lives in COZE_API_TOKEN (no credentials.json / refresh).
|
|
228
|
+
return os.environ.get("COZELOOP_API_TOKEN") or os.environ.get("COZE_API_TOKEN")
|
|
228
229
|
|
|
229
230
|
# -------------------------------------------------------------------------
|
|
230
231
|
|
|
@@ -45,7 +45,7 @@ from typing import Optional, List, Dict, Any
|
|
|
45
45
|
_COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
|
|
46
46
|
_COZE_API = "https://api.coze.cn"
|
|
47
47
|
_REFRESH_THRESHOLD = 10 * 60
|
|
48
|
-
_DEFAULT_WORKSPACE_ID = "
|
|
48
|
+
_DEFAULT_WORKSPACE_ID = "7644910356078837760" # hardcoded spaceID fallback
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
# --- coze-context parsing -------------------------------------------------
|
|
@@ -197,7 +197,8 @@ def get_fresh_token():
|
|
|
197
197
|
new_token = _refresh_token(creds["refresh_token"])
|
|
198
198
|
if new_token:
|
|
199
199
|
return new_token
|
|
200
|
-
|
|
200
|
+
# Cloud sandbox: token lives in COZE_API_TOKEN (no credentials.json / refresh).
|
|
201
|
+
return os.environ.get("COZELOOP_API_TOKEN") or os.environ.get("COZE_API_TOKEN")
|
|
201
202
|
# -------------------------------------------------------------------------
|
|
202
203
|
|
|
203
204
|
# --- SDK Import ---
|