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
- # @cozeloop/onboard
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 @cozeloop/onboard --agent=<type>
9
+ npx coze_lab --agent=<type>
10
10
 
11
11
  # Auth-only commands (no agent configuration)
12
- npx @cozeloop/onboard --login # Device Code login only
13
- npx @cozeloop/onboard --status # Show current authorization status
14
- npx @cozeloop/onboard --refresh # Force-refresh the access token
15
- npx @cozeloop/onboard --logout # Clear cached credentials
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 @cozeloop/onboard --agent=openclaw` to refresh it if OpenClaw reports auth errors.
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 @cozeloop/onboard --agent=${agent} ...`,
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
- function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir) {
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
- existing.env.COZELOOP_API_TOKEN = patToken;
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
- function writeCodexHook(token, workspaceId, pythonCmd, codexHome) {
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
- const envContent = [
4524
+ // 云端(cloud):不落明文 token,hook 运行时从环境变量 COZE_API_TOKEN 读取。
4525
+ const envLines = [
4477
4526
  `COZELOOP_WORKSPACE_ID=${workspaceId}`,
4478
- `COZELOOP_API_TOKEN=${token}`,
4479
- 'TRACE_TO_COZELOOP=true',
4480
- // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
4481
- `x_tt_env=${PPE_TT_ENV}`,
4482
- `x_use_ppe=${PPE_USE_PPE}`,
4483
- ].join('\n') + '\n';
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: 首次运行 Codex 时会提示信任 hook,请选择"信任"以启用 trace 上报。');
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
- function writeOpenClawHook(token, workspaceId) {
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
- authorization: `Bearer ${token}`,
4606
- endpoint: 'https://api.coze.cn/v1/loop/opentelemetry',
4607
- workspaceId,
4608
- debug: true,
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 已写入。请在当前 Claude Code 会话运行 /new(或重开会话)使 hook 生效——外部进程无法替你 /new。',
4928
- 'codex': 'Hook 已写入。请重开 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 (load cached → refresh → device code)
5003
- info('Step 1/5: 检查授权状态...');
5004
- let token = await getValidToken();
5005
- console.log('');
5006
-
5007
- // Step 2: Verify token works
5008
- info('Step 2/5: 验证工作空间权限...');
5009
- let verified = await verifyWorkspace(token);
5010
- if (verified === null) {
5011
- warn('Token 已失效,正在重新授权...');
5012
- deleteCredentials();
5013
- console.log('');
5014
- token = await deviceCodeAuth().then(c => c.access_token);
5015
- verified = await verifyWorkspace(token);
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 3: Detect agent binary
5023
- info('Step 3/5: Detecting agent...');
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 4: Environment checks
5028
- info('Step 4/5: Checking environment...');
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 5: Write hook configuration
5038
- info('Step 5/5: Writing hook configuration...');
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
- written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined);
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
- written = writeOpenClawHook(token, WORKSPACE_ID) || {};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -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 = "7645949103524380682" # hardcoded spaceID fallback
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
- return os.environ.get("COZELOOP_API_TOKEN")
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 = "7645949103524380682" # hardcoded spaceID fallback
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
- return os.environ.get("COZELOOP_API_TOKEN")
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 ---