coze_lab 0.1.5 → 0.1.6
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,6 +4190,7 @@ 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'] };
|
|
4160
4194
|
|
|
4161
4195
|
// 优先 --agent-id:读 ~/.coze/agents/<id>/config.json 的 framework 自动路由。
|
|
4162
4196
|
if (args['agent-id']) {
|
|
@@ -4167,6 +4201,8 @@ function validateArgs(args) {
|
|
|
4167
4201
|
workspace: resolved.workspace,
|
|
4168
4202
|
agentRoot: resolved.root,
|
|
4169
4203
|
'codex-home': args['codex-home'],
|
|
4204
|
+
pairCode: args['pair-code'],
|
|
4205
|
+
cloud: !!args['cloud'],
|
|
4170
4206
|
};
|
|
4171
4207
|
}
|
|
4172
4208
|
|
|
@@ -4183,6 +4219,8 @@ function validateArgs(args) {
|
|
|
4183
4219
|
' --login Login (Device Code flow)',
|
|
4184
4220
|
' --refresh Force refresh access token',
|
|
4185
4221
|
' --logout Clear cached credentials',
|
|
4222
|
+
' --verify Send a test trace to verify the reporting pipeline',
|
|
4223
|
+
' 可带 --pair-code=<值> 写入 trace metadata(缺省自动生成),供查询方回查',
|
|
4186
4224
|
]);
|
|
4187
4225
|
}
|
|
4188
4226
|
if (!VALID_AGENTS.includes(args['agent'])) {
|
|
@@ -4195,7 +4233,7 @@ function validateArgs(args) {
|
|
|
4195
4233
|
' --agent=openclaw',
|
|
4196
4234
|
]);
|
|
4197
4235
|
}
|
|
4198
|
-
return { agent: args['agent'], 'codex-home': args['codex-home'] };
|
|
4236
|
+
return { agent: args['agent'], 'codex-home': args['codex-home'], pairCode: args['pair-code'], cloud: !!args['cloud'] };
|
|
4199
4237
|
}
|
|
4200
4238
|
|
|
4201
4239
|
// ─── 4. Agent detection ──────────────────────────────────────────────────────
|
|
@@ -4341,7 +4379,7 @@ function checkVersionWhitelist(agent, version) {
|
|
|
4341
4379
|
'Trace reporting may not work correctly.',
|
|
4342
4380
|
'Please upgrade and re-run:',
|
|
4343
4381
|
` ${UPGRADE_CMD[agent]}`,
|
|
4344
|
-
` npx
|
|
4382
|
+
` npx coze_lab --agent=${agent} ...`,
|
|
4345
4383
|
]);
|
|
4346
4384
|
}
|
|
4347
4385
|
|
|
@@ -4386,7 +4424,9 @@ function mergeJson(filepath, mergeFn) {
|
|
|
4386
4424
|
// writeClaudeCodeHook 配置 Claude Code 的 hook。
|
|
4387
4425
|
// configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
|
|
4388
4426
|
// settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
|
|
4389
|
-
|
|
4427
|
+
// cloud=true 时不把明文 token 写进 settings.local.json —— 云端 hook 运行时直接读
|
|
4428
|
+
// 环境变量 COZE_API_TOKEN(见 scripts/claude-code/cozeloop_hook.py 的 get_fresh_token)。
|
|
4429
|
+
function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cloud) {
|
|
4390
4430
|
const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
|
|
4391
4431
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
4392
4432
|
const baseDir = configBaseDir || process.cwd();
|
|
@@ -4439,7 +4479,13 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir) {
|
|
|
4439
4479
|
const localSettings = mergeJson(localSettingsPath, (existing) => {
|
|
4440
4480
|
if (!existing.env) existing.env = {};
|
|
4441
4481
|
existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
|
|
4442
|
-
|
|
4482
|
+
// 云端:token 由 sandbox 注入到环境变量 COZE_API_TOKEN,hook 运行时直接读,
|
|
4483
|
+
// 不在配置文件落明文 token。本地:写入 OAuth 拿到的 token。
|
|
4484
|
+
if (cloud) {
|
|
4485
|
+
delete existing.env.COZELOOP_API_TOKEN;
|
|
4486
|
+
} else {
|
|
4487
|
+
existing.env.COZELOOP_API_TOKEN = patToken;
|
|
4488
|
+
}
|
|
4443
4489
|
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4444
4490
|
existing.env.x_tt_env = PPE_TT_ENV;
|
|
4445
4491
|
existing.env.x_use_ppe = PPE_USE_PPE;
|
|
@@ -4458,7 +4504,9 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir) {
|
|
|
4458
4504
|
|
|
4459
4505
|
// writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
|
|
4460
4506
|
// 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
|
|
4461
|
-
|
|
4507
|
+
// cloud=true 时 cozeloop.env 不写死 token —— 云端 hook 运行时直接读环境变量
|
|
4508
|
+
// COZE_API_TOKEN(见 scripts/codex/cozeloop_hook.py 的 get_fresh_token)。
|
|
4509
|
+
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
4462
4510
|
const home = codexHome || path.join(os.homedir(), '.codex');
|
|
4463
4511
|
const hooksDir = path.join(home, 'hooks');
|
|
4464
4512
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
@@ -4473,14 +4521,18 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome) {
|
|
|
4473
4521
|
writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
|
|
4474
4522
|
|
|
4475
4523
|
// 2. Write env file with chmod 600
|
|
4476
|
-
|
|
4524
|
+
// 云端(cloud):不落明文 token,hook 运行时从环境变量 COZE_API_TOKEN 读取。
|
|
4525
|
+
const envLines = [
|
|
4477
4526
|
`COZELOOP_WORKSPACE_ID=${workspaceId}`,
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4527
|
+
];
|
|
4528
|
+
if (!cloud) {
|
|
4529
|
+
envLines.push(`COZELOOP_API_TOKEN=${token}`);
|
|
4530
|
+
}
|
|
4531
|
+
envLines.push('TRACE_TO_COZELOOP=true');
|
|
4532
|
+
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4533
|
+
envLines.push(`x_tt_env=${PPE_TT_ENV}`);
|
|
4534
|
+
envLines.push(`x_use_ppe=${PPE_USE_PPE}`);
|
|
4535
|
+
const envContent = envLines.join('\n') + '\n';
|
|
4484
4536
|
try {
|
|
4485
4537
|
fs.writeFileSync(envFile, envContent, { mode: 0o600 });
|
|
4486
4538
|
} catch (e) {
|
|
@@ -4518,12 +4570,16 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome) {
|
|
|
4518
4570
|
errorBox([`ERROR: Cannot write ${hooksJson}`, '', e.message]);
|
|
4519
4571
|
}
|
|
4520
4572
|
ok(`Hook registered in ${hooksJson}`);
|
|
4521
|
-
warn('Codex hook trust:
|
|
4573
|
+
warn('Codex hook trust: 首次启动 Codex 会提示 "Hooks need review"。在该提示按 t(Trust all and continue),或在会话内运行 /hooks 后按 t,即可一次性信任全部 hook 启用 trace 上报。');
|
|
4522
4574
|
|
|
4523
4575
|
return { hookScript, envFile, hooksJson };
|
|
4524
4576
|
}
|
|
4525
4577
|
|
|
4526
|
-
|
|
4578
|
+
// writeOpenClawHook 配置 OpenClaw 的 cozeloop-trace 插件(全局装在 ~/.openclaw)。
|
|
4579
|
+
// agentId 非空时并入 plugins.entries[...].config.traceAgentIds allowlist —— 插件运行时
|
|
4580
|
+
// 用 resolveAgentIdFromHookCtx 取当前 agentId,仅 allowlist 内的 agent 才上报 trace。
|
|
4581
|
+
// allowlist 为空(本地全局模式)= 全部放行,向后兼容。
|
|
4582
|
+
function writeOpenClawHook(token, workspaceId, agentId) {
|
|
4527
4583
|
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
4528
4584
|
const pluginDir = path.join(os.homedir(), '.cozeloop', 'openclaw-plugin');
|
|
4529
4585
|
|
|
@@ -4601,12 +4657,25 @@ function writeOpenClawHook(token, workspaceId) {
|
|
|
4601
4657
|
existing.plugins.entries[PLUGIN].enabled = true;
|
|
4602
4658
|
// hooks.allowConversationAccess required for 2026.5+ to access session content
|
|
4603
4659
|
existing.plugins.entries[PLUGIN].hooks = { allowConversationAccess: true };
|
|
4604
|
-
existing.plugins.entries[PLUGIN].config = {
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4660
|
+
if (!existing.plugins.entries[PLUGIN].config) existing.plugins.entries[PLUGIN].config = {};
|
|
4661
|
+
const pcfg = existing.plugins.entries[PLUGIN].config;
|
|
4662
|
+
pcfg.authorization = `Bearer ${token}`;
|
|
4663
|
+
pcfg.endpoint = 'https://api.coze.cn/v1/loop/opentelemetry';
|
|
4664
|
+
pcfg.workspaceId = workspaceId;
|
|
4665
|
+
pcfg.debug = true;
|
|
4666
|
+
// per-agent trace 放行:把当前 agentId 并入 traceAgentIds(去重、归一为小写,
|
|
4667
|
+
// 与插件侧 resolveAgentIdFromHookCtx 的归一一致)。无 agentId(全局模式)则不动
|
|
4668
|
+
// allowlist —— 空 allowlist 表示全部放行。
|
|
4669
|
+
if (agentId) {
|
|
4670
|
+
const norm = String(agentId).trim().toLowerCase();
|
|
4671
|
+
// 读 existing 时一并归一(小写、去空),与插件侧 resolveAgentIdFromHookCtx 一致,
|
|
4672
|
+
// 防手工编辑混入大写条目导致去重失效。
|
|
4673
|
+
const list = (Array.isArray(pcfg.traceAgentIds) ? pcfg.traceAgentIds : [])
|
|
4674
|
+
.map((s) => String(s).trim().toLowerCase())
|
|
4675
|
+
.filter(Boolean);
|
|
4676
|
+
if (norm && !list.includes(norm)) list.push(norm);
|
|
4677
|
+
pcfg.traceAgentIds = list;
|
|
4678
|
+
}
|
|
4610
4679
|
return existing;
|
|
4611
4680
|
});
|
|
4612
4681
|
|
|
@@ -4632,15 +4701,17 @@ function writeOpenClawHook(token, workspaceId) {
|
|
|
4632
4701
|
|
|
4633
4702
|
// ─── 8. Auth — Device Code OAuth + token store ───────────────────────────────
|
|
4634
4703
|
const https = require('https');
|
|
4704
|
+
const crypto = require('crypto');
|
|
4635
4705
|
|
|
4636
|
-
function httpsPost(url, body) {
|
|
4706
|
+
function httpsPost(url, body, extraHeaders) {
|
|
4637
4707
|
return new Promise((resolve, reject) => {
|
|
4638
4708
|
const data = JSON.stringify(body);
|
|
4639
4709
|
const u = new URL(url);
|
|
4640
4710
|
const req = https.request(
|
|
4641
4711
|
{ hostname: u.hostname, path: u.pathname + u.search, method: 'POST',
|
|
4642
4712
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data),
|
|
4643
|
-
'x-tt-env': PPE_TT_ENV, 'x-use-ppe': PPE_USE_PPE
|
|
4713
|
+
'x-tt-env': PPE_TT_ENV, 'x-use-ppe': PPE_USE_PPE,
|
|
4714
|
+
...(extraHeaders || {}) } },
|
|
4644
4715
|
(res) => {
|
|
4645
4716
|
let buf = '';
|
|
4646
4717
|
res.on('data', c => buf += c);
|
|
@@ -4654,6 +4725,66 @@ function httpsPost(url, body) {
|
|
|
4654
4725
|
});
|
|
4655
4726
|
}
|
|
4656
4727
|
|
|
4728
|
+
// 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
|
|
4729
|
+
// 只看 HTTP 状态码(2xx=通),不回查 trace 是否落库——回查由外部查询方完成。
|
|
4730
|
+
// pairCode 写进 span 的 pair_code attribute,供查询方按该字段过滤回查;缺省自动生成。
|
|
4731
|
+
// 不在函数内退出,退出行为交给调用方(主流程 Step 6 / 独立命令 --verify)。
|
|
4732
|
+
async function verifyTraceReport(token, workspaceId, pairCode) {
|
|
4733
|
+
const traceId = crypto.randomBytes(16).toString('hex'); // 32 hex chars
|
|
4734
|
+
const spanId = crypto.randomBytes(8).toString('hex'); // 16 hex chars
|
|
4735
|
+
const nowNs = String(Date.now() * 1_000_000); // OTLP 要求纳秒 unix 时间(字符串)
|
|
4736
|
+
// 缺省自动生成 12 位 hex 配对码;查询方按 field_name="pair_code" 回查。
|
|
4737
|
+
const pair = pairCode || crypto.randomBytes(6).toString('hex');
|
|
4738
|
+
|
|
4739
|
+
const otlpBody = {
|
|
4740
|
+
resourceSpans: [{
|
|
4741
|
+
resource: {
|
|
4742
|
+
attributes: [
|
|
4743
|
+
{ key: 'service.name', value: { stringValue: 'cozelab-onboard' } },
|
|
4744
|
+
],
|
|
4745
|
+
},
|
|
4746
|
+
scopeSpans: [{
|
|
4747
|
+
scope: { name: 'cozelab-onboard-selfcheck' },
|
|
4748
|
+
spans: [{
|
|
4749
|
+
traceId,
|
|
4750
|
+
spanId,
|
|
4751
|
+
name: 'cozelab-onboard-selfcheck',
|
|
4752
|
+
kind: 1,
|
|
4753
|
+
startTimeUnixNano: nowNs,
|
|
4754
|
+
endTimeUnixNano: nowNs,
|
|
4755
|
+
status: { code: 1 },
|
|
4756
|
+
attributes: [
|
|
4757
|
+
{ key: 'pair_code', value: { stringValue: pair } },
|
|
4758
|
+
],
|
|
4759
|
+
}],
|
|
4760
|
+
}],
|
|
4761
|
+
}],
|
|
4762
|
+
};
|
|
4763
|
+
|
|
4764
|
+
let res;
|
|
4765
|
+
try {
|
|
4766
|
+
res = await httpsPost(
|
|
4767
|
+
`${COZE_API}/v1/loop/opentelemetry/v1/traces`,
|
|
4768
|
+
otlpBody,
|
|
4769
|
+
{ Authorization: `Bearer ${token}`, 'cozeloop-workspace-id': workspaceId },
|
|
4770
|
+
);
|
|
4771
|
+
} catch (e) {
|
|
4772
|
+
warn(`trace 上报失败: ${e.message}`);
|
|
4773
|
+
return { success: false, status: 0, body: e.message, traceId, pairCode: pair };
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
const success = res.status >= 200 && res.status < 300;
|
|
4777
|
+
if (success) {
|
|
4778
|
+
ok(`trace 上报成功 (traceId=${traceId}, pair_code=${pair})`);
|
|
4779
|
+
info(`查询方可用 pair_code=${pair} 在 CozeLoop 回查确认该 trace 已落库。`);
|
|
4780
|
+
} else {
|
|
4781
|
+
warn(`trace 上报失败: HTTP ${res.status}`);
|
|
4782
|
+
const snippet = (res.body || '').slice(0, 300);
|
|
4783
|
+
if (snippet) console.log(snippet);
|
|
4784
|
+
}
|
|
4785
|
+
return { success, status: res.status, body: res.body, traceId, pairCode: pair };
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4657
4788
|
function httpsGet(url, headers) {
|
|
4658
4789
|
return new Promise((resolve, reject) => {
|
|
4659
4790
|
// 合并 PPE 泳道 header
|
|
@@ -4911,21 +5042,10 @@ function authStatus() {
|
|
|
4911
5042
|
box(lines, statusColor);
|
|
4912
5043
|
}
|
|
4913
5044
|
|
|
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
5045
|
// ─── 9. Main ─────────────────────────────────────────────────────────────────
|
|
4926
5046
|
const NEXT_STEP = {
|
|
4927
|
-
'claude-code': 'Hook
|
|
4928
|
-
'codex': 'Hook
|
|
5047
|
+
'claude-code': 'Hook 已写入。Claude Code 会自动热重载 hooks,当前会话即刻生效,无需 /new 或重启。',
|
|
5048
|
+
'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex 会话(已配置 SessionStart hook,新会话自动加载)。',
|
|
4929
5049
|
'openclaw': 'OpenClaw gateway 已自动重启,trace 即刻生效。',
|
|
4930
5050
|
};
|
|
4931
5051
|
|
|
@@ -4991,7 +5111,17 @@ async function main() {
|
|
|
4991
5111
|
return;
|
|
4992
5112
|
}
|
|
4993
5113
|
|
|
5114
|
+
if (args.verify) {
|
|
5115
|
+
info('验证 trace 上报链路...');
|
|
5116
|
+
const token = await getValidToken(); // 无凭证会自动走登录/刷新
|
|
5117
|
+
console.log('');
|
|
5118
|
+
const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode);
|
|
5119
|
+
process.exit(result.success ? 0 : 1);
|
|
5120
|
+
}
|
|
5121
|
+
|
|
4994
5122
|
const { agent } = args;
|
|
5123
|
+
// 云端模式:开启结构化输出 + errorBox 抛异常(而非 exit)。
|
|
5124
|
+
CLOUD_MODE = !!args.cloud;
|
|
4995
5125
|
// per-agent 路由时 agent 的 workspace(claude-code 用它做配置根目录)。
|
|
4996
5126
|
const agentWorkspace = args.workspace || '';
|
|
4997
5127
|
if (args.agentId) {
|
|
@@ -4999,33 +5129,35 @@ async function main() {
|
|
|
4999
5129
|
console.log('');
|
|
5000
5130
|
}
|
|
5001
5131
|
|
|
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 仍无效,请联系管理员。']);
|
|
5132
|
+
// Step 1: Authorize.
|
|
5133
|
+
// 云端(--cloud):token 取自环境变量 COZE_API_TOKEN,跳过 OAuth / credentials.json。
|
|
5134
|
+
// 本地:load cached → refresh → device code。
|
|
5135
|
+
let token;
|
|
5136
|
+
if (args.cloud) {
|
|
5137
|
+
info('Step 1/5: 云端模式,从环境变量 COZE_API_TOKEN 读取 token...');
|
|
5138
|
+
token = process.env.COZE_API_TOKEN;
|
|
5139
|
+
if (!token) {
|
|
5140
|
+
errorBox([
|
|
5141
|
+
'ERROR: --cloud 模式要求环境变量 COZE_API_TOKEN',
|
|
5142
|
+
'',
|
|
5143
|
+
'云端 sandbox 应在进程环境中注入 COZE_API_TOKEN。',
|
|
5144
|
+
'未检测到该变量,无法配置 trace 上报。',
|
|
5145
|
+
]);
|
|
5018
5146
|
}
|
|
5147
|
+
ok('已从环境变量读取 COZE_API_TOKEN');
|
|
5148
|
+
} else {
|
|
5149
|
+
info('Step 1/5: 检查授权状态...');
|
|
5150
|
+
token = await getValidToken();
|
|
5019
5151
|
}
|
|
5020
5152
|
console.log('');
|
|
5021
5153
|
|
|
5022
|
-
// Step
|
|
5023
|
-
info('Step
|
|
5154
|
+
// Step 2: Detect agent binary
|
|
5155
|
+
info('Step 2/5: Detecting agent...');
|
|
5024
5156
|
const version = detectAgent(agent);
|
|
5025
5157
|
console.log('');
|
|
5026
5158
|
|
|
5027
|
-
// Step
|
|
5028
|
-
info('Step
|
|
5159
|
+
// Step 3: Environment checks
|
|
5160
|
+
info('Step 3/5: Checking environment...');
|
|
5029
5161
|
let pythonCmd = null;
|
|
5030
5162
|
if (agent !== 'openclaw') {
|
|
5031
5163
|
pythonCmd = checkPython();
|
|
@@ -5034,20 +5166,33 @@ async function main() {
|
|
|
5034
5166
|
checkVersionWhitelist(agent, version);
|
|
5035
5167
|
console.log('');
|
|
5036
5168
|
|
|
5037
|
-
// Step
|
|
5038
|
-
info('Step
|
|
5169
|
+
// Step 4: Write hook configuration
|
|
5170
|
+
info('Step 4/5: Writing hook configuration...');
|
|
5039
5171
|
let written = {};
|
|
5040
5172
|
if (agent === 'claude-code') {
|
|
5041
5173
|
// per-agent 路由时把配置写进 agent 的 workspace(仅该 agent 生效)。
|
|
5042
|
-
|
|
5174
|
+
// 云端按 agentId 路由但 workspace 为空会导致 settings.json 落 ~/.claude、
|
|
5175
|
+
// settings.local.json 落进程 cwd —— per-agent 隔离失效。此时显式报错。
|
|
5176
|
+
if (args.agentId && !agentWorkspace) {
|
|
5177
|
+
errorBox([
|
|
5178
|
+
`ERROR: agent "${args.agentId}" 的 config.json 缺少 workspace 字段`,
|
|
5179
|
+
'',
|
|
5180
|
+
'claude-code per-agent 配置需要明确的 workspace 目录,',
|
|
5181
|
+
'否则 hook 配置会落到全局/进程目录,无法做到 per-agent 隔离。',
|
|
5182
|
+
]);
|
|
5183
|
+
}
|
|
5184
|
+
written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud);
|
|
5043
5185
|
} else if (agent === 'codex') {
|
|
5044
5186
|
// CODEX_HOME 来源优先级:--codex-home= > 环境变量 CODEX_HOME > ~/.codex(缺省)。
|
|
5045
5187
|
const codexHome = args['codex-home'] || process.env.CODEX_HOME || undefined;
|
|
5046
5188
|
if (codexHome) info(`Codex 配置目录: ${codexHome}`);
|
|
5047
|
-
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome);
|
|
5189
|
+
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
|
|
5048
5190
|
} else {
|
|
5049
|
-
|
|
5191
|
+
// openclaw:云端用 traceAgentIds allowlist 做 per-agent 放行。
|
|
5192
|
+
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId) || {};
|
|
5050
5193
|
}
|
|
5194
|
+
// 走到这里说明 detectAgent / 环境检查 / 写 hook 配置全部成功 → 注入成功。
|
|
5195
|
+
cloudResult.inject = 'ok';
|
|
5051
5196
|
console.log('');
|
|
5052
5197
|
|
|
5053
5198
|
// Success summary(用实际写入路径,per-agent / 自定义 CODEX_HOME 时也准确)
|
|
@@ -5079,9 +5224,52 @@ async function main() {
|
|
|
5079
5224
|
info('确保 CozeLoop Token 具备 Trace ingest 权限,否则 hook 触发但上报会失败。');
|
|
5080
5225
|
console.log('');
|
|
5081
5226
|
|
|
5227
|
+
// Step 5: Verify trace reporting end-to-end
|
|
5228
|
+
info('Step 5/5: 验证 trace 上报链路...');
|
|
5229
|
+
const verifyResult = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode);
|
|
5230
|
+
if (verifyResult.success) {
|
|
5231
|
+
cloudResult.verify = 'ok';
|
|
5232
|
+
} else if (CLOUD_MODE) {
|
|
5233
|
+
// 云端:注入已成功,验证失败不阻断(放行),记录结果供后台弹 warning。
|
|
5234
|
+
cloudResult.verify = 'fail';
|
|
5235
|
+
cloudResult.logid = extractLogid(verifyResult.body) || cloudResult.logid;
|
|
5236
|
+
cloudResult.message = `trace 上报自检失败 HTTP ${verifyResult.status}: ${(verifyResult.body || '').slice(0, 200)}`;
|
|
5237
|
+
warn('trace 上报自检失败,但 hook 配置已写入(云端放行)。');
|
|
5238
|
+
console.log('');
|
|
5239
|
+
emitCloudResult();
|
|
5240
|
+
successBox(summaryLines);
|
|
5241
|
+
return;
|
|
5242
|
+
} else {
|
|
5243
|
+
errorBox([
|
|
5244
|
+
'ERROR: trace 上报自检失败',
|
|
5245
|
+
'',
|
|
5246
|
+
`HTTP ${verifyResult.status}`,
|
|
5247
|
+
(verifyResult.body || '').slice(0, 300),
|
|
5248
|
+
'',
|
|
5249
|
+
'hook 配置已写入,但上报链路未打通。',
|
|
5250
|
+
'请确认 CozeLoop Token 具备 Trace ingest 权限,再重试。',
|
|
5251
|
+
]); // errorBox 内部 process.exit(1)
|
|
5252
|
+
}
|
|
5253
|
+
console.log('');
|
|
5254
|
+
|
|
5255
|
+
emitCloudResult();
|
|
5082
5256
|
successBox(summaryLines);
|
|
5083
5257
|
}
|
|
5084
5258
|
|
|
5085
5259
|
main().catch(e => {
|
|
5260
|
+
// 云端模式:失败时输出结构化结果(含 logid),exit 非 0 但带 COZE_LAB_RESULT 行。
|
|
5261
|
+
if (CLOUD_MODE) {
|
|
5262
|
+
if (cloudResult.inject !== 'ok') {
|
|
5263
|
+
// 写 hook 配置前的任何失败 → 注入失败。
|
|
5264
|
+
cloudResult.inject = 'fail';
|
|
5265
|
+
} else if (cloudResult.verify === 'skip') {
|
|
5266
|
+
// 注入已成功,但验证阶段异常崩溃(未走到正常的 ok/fail 判定)→ 记为验证失败。
|
|
5267
|
+
cloudResult.verify = 'fail';
|
|
5268
|
+
}
|
|
5269
|
+
if (!cloudResult.message) cloudResult.message = e && e.message ? e.message : 'unexpected failure';
|
|
5270
|
+
if (!cloudResult.logid) cloudResult.logid = extractLogid(cloudResult.message);
|
|
5271
|
+
emitCloudResult();
|
|
5272
|
+
process.exit(1);
|
|
5273
|
+
}
|
|
5086
5274
|
errorBox(['ERROR: Unexpected failure', '', e.message]);
|
|
5087
5275
|
});
|
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 ---
|