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
- # @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,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
- // 优先 --agent-id:读 ~/.coze/agents/<id>/config.json 的 framework 自动路由。
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 @cozeloop/onboard --agent=${agent} ...`,
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
- function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir) {
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
- existing.env.COZELOOP_API_TOKEN = patToken;
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
- function writeCodexHook(token, workspaceId, pythonCmd, codexHome) {
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
- const envContent = [
4546
+ // 云端(cloud):不落明文 token,hook 运行时从环境变量 COZE_API_TOKEN 读取。
4547
+ const envLines = [
4477
4548
  `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';
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: 首次运行 Codex 时会提示信任 hook,请选择"信任"以启用 trace 上报。');
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
- function writeOpenClawHook(token, workspaceId) {
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
- authorization: `Bearer ${token}`,
4606
- endpoint: 'https://api.coze.cn/v1/loop/opentelemetry',
4607
- workspaceId,
4608
- debug: true,
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 已写入。请在当前 Claude Code 会话运行 /new(或重开会话)使 hook 生效——外部进程无法替你 /new。',
4928
- 'codex': 'Hook 已写入。请重开 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
- const agentWorkspace = args.workspace || '';
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 (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 仍无效,请联系管理员。']);
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 3: Detect agent binary
5023
- info('Step 3/5: Detecting agent...');
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 4: Environment checks
5028
- info('Step 4/5: Checking environment...');
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 5: Write hook configuration
5038
- info('Step 5/5: Writing hook configuration...');
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
- written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined);
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
- written = writeOpenClawHook(token, WORKSPACE_ID) || {};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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 ---