coze_lab 0.1.44 → 0.1.46

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/index.js CHANGED
@@ -2,13 +2,9 @@
2
2
  'use strict';
3
3
 
4
4
  // ─── 0. Constants ─────────────────────────────────────────────────────────────
5
- const CLIENT_ID = '08972682140163281554629748278108.app.coze';
6
5
  const WORKSPACE_ID = '7649231955045072915';
7
6
  const COZE_API = 'https://api.coze.cn';
8
- const CREDS_PATH = require('path').join(require('os').homedir(), '.cozeloop', 'credentials.json');
9
7
  const PACKAGE_VERSION = require('./package.json').version;
10
- // Refresh when less than 10 minutes remain
11
- const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
12
8
  const TOKEN_SOURCE_AGENT_PAT = 'agent_config.patToken';
13
9
 
14
10
  // ─── 1. Cloud structured output ──────────────────────────────────────────────
@@ -118,7 +114,7 @@ const OPENCLAW_PLUGIN_VERSION = (() => {
118
114
  }
119
115
  })();
120
116
 
121
- // Read a single script file (Python hook / refresh) as a UTF-8 string.
117
+ // Read a single script file as a UTF-8 string.
122
118
  function readScript(relPath) {
123
119
  return require('fs').readFileSync(require('path').join(SCRIPTS_DIR, relPath), 'utf8');
124
120
  }
@@ -147,6 +143,8 @@ function loadOpenclawFiles() {
147
143
  function parseArgs() {
148
144
  const args = {};
149
145
  for (const arg of process.argv.slice(2)) {
146
+ // Legacy auth-only commands are still parsed so validateArgs can return a
147
+ // clear "removed" error instead of falling through to generic usage.
150
148
  if (arg === '--logout') { args['logout'] = true; continue; }
151
149
  if (arg === '--login') { args['login'] = true; continue; }
152
150
  if (arg === '--status') { args['status'] = true; continue; }
@@ -225,11 +223,16 @@ function resolveAgent(agentId, soft) {
225
223
  }
226
224
 
227
225
  function validateArgs(args) {
228
- if (args['logout']) return { logout: true };
229
- if (args['login']) return { login: true };
230
- if (args['status']) return { status: true };
231
- if (args['refresh']) return { refresh: true };
232
- if (args['verify']) return { verify: true, pairCode: args['pair-code'] };
226
+ const legacyAuthCmd = ['login', 'status', 'refresh', 'logout'].find((name) => args[name]);
227
+ if (legacyAuthCmd) {
228
+ errorBox([
229
+ `ERROR: --${legacyAuthCmd} 已移除`,
230
+ '',
231
+ 'coze_lab 不再提供 Device Code / OAuth 本地授权兜底链路。',
232
+ '本地请使用 --agent-id=<id>,并确保 ~/.coze/agents/<id>/config.json 中存在 patToken。',
233
+ '云端请通过环境变量 COZELOOP_API_TOKEN 或 COZE_API_TOKEN 注入 trace token。',
234
+ ]);
235
+ }
233
236
 
234
237
  // --agent-id:优先读 coze-bridge 的 ~/.coze/agents/<id>/config.json 拿 framework/workspace。
235
238
  // 云端判定优先看 deployType / CLOUD_ENV;兼容老 config 时再看 cloud-only 落盘字段。
@@ -251,6 +254,7 @@ function validateArgs(args) {
251
254
  pairCode: args['pair-code'],
252
255
  cloud,
253
256
  force: !!args['force'],
257
+ verify: !!args['verify'],
254
258
  };
255
259
  }
256
260
  // 显式 --cloud 或 CLOUD_ENV=1 且 config.json 缺失:回退到显式 --agent
@@ -274,6 +278,15 @@ function validateArgs(args) {
274
278
  pairCode: args['pair-code'],
275
279
  cloud: true,
276
280
  force: !!args['force'],
281
+ verify: !!args['verify'],
282
+ };
283
+ }
284
+
285
+ if (args['verify']) {
286
+ return {
287
+ verify: true,
288
+ pairCode: args['pair-code'],
289
+ cloud: !!args['cloud'] || isCloudRuntimeEnv() || !!getCloudTokenInfo().token,
277
290
  };
278
291
  }
279
292
 
@@ -282,18 +295,15 @@ function validateArgs(args) {
282
295
  'ERROR: --agent 或 --agent-id 至少提供一个',
283
296
  '',
284
297
  'Usage:',
285
- ' --agent=claude-code | codex | openclaw (全局配置)',
286
- ' --agent-id=<id> ( ~/.coze/agents/<id> 的 framework 自动路由)',
298
+ ' --agent-id=<id> (本地/云端按 ~/.coze/agents/<id>/config.json 自动路由)',
299
+ ' --cloud --agent=claude-code|codex|openclaw (云端兼容调用,token 来自环境变量)',
287
300
  '',
288
301
  'Flags:',
289
302
  ' --force 强制重装(OpenClaw 跳过幂等检查,无条件重写插件 + 重装依赖 + 重启 gateway)',
290
303
  '',
291
304
  'Other commands:',
292
- ' --status Show authorization status',
293
- ' --login Login (Device Code flow)',
294
- ' --refresh Force refresh access token',
295
- ' --logout Clear cached credentials',
296
305
  ' --verify Send a test trace to verify the reporting pipeline',
306
+ ' 需要 --agent-id 的 patToken,或环境变量 COZELOOP_API_TOKEN/COZE_API_TOKEN',
297
307
  ' 可带 --pair-code=<值> 写入 trace metadata(缺省自动生成),供查询方回查',
298
308
  ]);
299
309
  }
@@ -307,7 +317,17 @@ function validateArgs(args) {
307
317
  ' --agent=openclaw',
308
318
  ]);
309
319
  }
310
- return { agent: args['agent'], 'codex-home': args['codex-home'], pairCode: args['pair-code'], cloud: !!args['cloud'], force: !!args['force'] };
320
+ const cloud = !!args['cloud'] || isCloudRuntimeEnv();
321
+ if (!cloud) {
322
+ errorBox([
323
+ 'ERROR: 本地模式不再支持仅使用 --agent=<type> 配置 trace',
324
+ '',
325
+ '本地配置必须通过 --agent-id=<id> 读取对应 agent config 中的 patToken。',
326
+ '请使用:',
327
+ ' npx coze_lab --agent-id=<id>',
328
+ ]);
329
+ }
330
+ return { agent: args['agent'], 'codex-home': args['codex-home'], pairCode: args['pair-code'], cloud, force: !!args['force'] };
311
331
  }
312
332
 
313
333
  // ─── 4. Agent detection ──────────────────────────────────────────────────────
@@ -439,12 +459,16 @@ function checkCozeloopSdk(pythonCmd, options = {}) {
439
459
  }
440
460
 
441
461
  // ─── 6. Version whitelist check ──────────────────────────────────────────────
442
- // Match on major.minor prefix: '1.3' matches 1.3.0, 1.3.2, 1.3.9
443
- const VERSION_WHITELIST = {
444
- 'claude-code': ['1.2', '1.3', '1.4', '2.0', '2.1'],
445
- 'codex': ['1.0', '1.1', '0.134'],
446
- 'openclaw': ['2026.3', '2026.4', '2026.5'],
462
+ // Forward-compatible lower bounds. Newer CLI releases are treated as
463
+ // supported by default because the hook selfcheck is the authoritative guard.
464
+ const VERSION_SUPPORT = {
465
+ 'claude-code': { min: '1.0.0', display: ['>= 1.0.0'] },
466
+ 'codex': { min: '0.134.0', display: ['>= 0.134.0'] },
467
+ 'openclaw': { min: '2026.3.0', display: ['>= 2026.3.0'] },
447
468
  };
469
+ const VERSION_WHITELIST = Object.fromEntries(
470
+ Object.entries(VERSION_SUPPORT).map(([agent, cfg]) => [agent, cfg.display]),
471
+ );
448
472
 
449
473
  const UPGRADE_CMD = {
450
474
  'claude-code': 'npm install -g @anthropic-ai/claude-code@latest',
@@ -453,15 +477,15 @@ const UPGRADE_CMD = {
453
477
  };
454
478
 
455
479
  function checkVersionWhitelist(agent, version) {
456
- const whitelist = VERSION_WHITELIST[agent] || [];
457
- const supported = whitelist.some(prefix => version.startsWith(prefix + '.') || version === prefix);
480
+ const support = VERSION_SUPPORT[agent];
481
+ const supported = support && isVersionAtLeast(version, support.min);
458
482
 
459
483
  if (supported) {
460
484
  ok(`${agent} v${version} — OK`);
461
485
  return;
462
486
  }
463
487
 
464
- const supportedList = whitelist.map(v => ` • ${v}.x`);
488
+ const supportedList = (support?.display || []).map(v => ` • ${v}`);
465
489
  warnBox([
466
490
  `⚠ WARNING: Unsupported ${agent} version: ${version}`,
467
491
  '',
@@ -471,10 +495,29 @@ function checkVersionWhitelist(agent, version) {
471
495
  'Trace reporting may not work correctly.',
472
496
  'Please upgrade and re-run:',
473
497
  ` ${UPGRADE_CMD[agent]}`,
474
- ` npx coze_lab --agent=${agent} ...`,
498
+ ' npx coze_lab --agent-id=<id>',
475
499
  ]);
476
500
  }
477
501
 
502
+ function parseVersionNumbers(version) {
503
+ const m = String(version || '').match(/(\d+(?:\.\d+){0,3})/);
504
+ return m ? m[1].split('.').map((part) => Number(part)) : [];
505
+ }
506
+
507
+ function isVersionAtLeast(version, minVersion) {
508
+ const got = parseVersionNumbers(version);
509
+ const min = parseVersionNumbers(minVersion);
510
+ if (!got.length || !min.length) return false;
511
+ const len = Math.max(got.length, min.length);
512
+ for (let i = 0; i < len; i += 1) {
513
+ const a = got[i] || 0;
514
+ const b = min[i] || 0;
515
+ if (a > b) return true;
516
+ if (a < b) return false;
517
+ }
518
+ return true;
519
+ }
520
+
478
521
  // ─── 7. Hook writers ─────────────────────────────────────────────────────────
479
522
  const fs = require('fs');
480
523
  const path = require('path');
@@ -545,15 +588,13 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
545
588
  const settingsPath = path.join(claudeDir, 'settings.json');
546
589
  const localSettingsPath = path.join(baseDir, '.claude', 'settings.local.json');
547
590
 
548
- // 1. Write Python hook scripts (trace + refresh) — 脚本放全局 ~/.claude/hooks,可共享
591
+ // 1. Write Python hook script — 脚本放全局 ~/.claude/hooks,可共享
549
592
  ensureDir(hooksDir);
550
593
  writeHookScript(hookScript, readScript('claude-code/cozeloop_hook.py'));
551
- const refreshScript = path.join(hooksDir, 'cozeloop_refresh.py');
552
- writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
553
594
 
554
- // 2. Merge settings.json — Stop (trace 收尾) + PostToolUse (trace 增量) + UserPromptSubmit (refresh)。用绝对路径。
595
+ // 2. Merge settings.json — Stop (trace 收尾) + PostToolUse (trace 增量)。用绝对路径。
596
+ // 同时清掉旧版本写入的 cozeloop_refresh.py hook,避免继续走 OAuth refresh 兜底。
555
597
  const hookCmd = `${pythonCmd} ${hookScript}`;
556
- const refreshCmd = `${pythonCmd} ${refreshScript}`;
557
598
 
558
599
  ensureDir(claudeDir);
559
600
  let settings;
@@ -576,12 +617,11 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
576
617
  );
577
618
  existing.hooks.PostToolUse.push({ matcher: '', hooks: [{ type: 'command', command: hookCmd }] });
578
619
 
579
- // UserPromptSubmit hook token refresh before each user message
620
+ // Remove legacy token-refresh hook entries.
580
621
  if (!existing.hooks.UserPromptSubmit) existing.hooks.UserPromptSubmit = [];
581
622
  existing.hooks.UserPromptSubmit = existing.hooks.UserPromptSubmit.filter(
582
623
  entry => !entry.hooks?.some(h => h.command?.includes('cozeloop_refresh.py'))
583
624
  );
584
- existing.hooks.UserPromptSubmit.push({ matcher: '', hooks: [{ type: 'command', command: refreshCmd }] });
585
625
 
586
626
  return existing;
587
627
  });
@@ -596,7 +636,7 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
596
636
  }
597
637
  ok(`Hook registered in ${settingsPath}`);
598
638
 
599
- // 3. Write credentials into <baseDir>/.claude/settings.local.json
639
+ // 3. Write hook environment into <baseDir>/.claude/settings.local.json
600
640
  ensureDir(path.join(baseDir, '.claude'));
601
641
  ensureDir(path.dirname(logFile));
602
642
  let localSettings;
@@ -648,9 +688,9 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
648
688
  try {
649
689
  atomicWriteFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2));
650
690
  } catch (e) {
651
- errorBox([`ERROR: Cannot write credentials to ${localSettingsPath}`, '', e.message]);
691
+ errorBox([`ERROR: Cannot write hook environment to ${localSettingsPath}`, '', e.message]);
652
692
  }
653
- ok(`Credentials written to ${localSettingsPath}`);
693
+ ok(`Hook environment written to ${localSettingsPath}`);
654
694
 
655
695
 
656
696
  return { hookScript, settingsPath, localSettingsPath, logFile };
@@ -658,8 +698,7 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
658
698
 
659
699
  // writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
660
700
  // 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
661
- // 普通本地模式不把短期 token 写入 cozeloop.env;Hook 运行时读取 ~/.cozeloop/credentials.json。
662
- // 本地 --agent-id 且 config.patToken 存在时写入该 PAT,并用 COZELOOP_TOKEN_SOURCE 标记来源。
701
+ // 本地 --agent-id config.patToken 写入 token,并用 COZELOOP_TOKEN_SOURCE 标记来源。
663
702
  // cloud=true 时写 COZELAB_ONBOARD_CLOUD,并带入 sandbox 注入的 trace token。
664
703
  function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSource) {
665
704
  const home = codexHome || path.join(os.homedir(), '.codex');
@@ -669,12 +708,9 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
669
708
  const logFile = path.join(hooksDir, 'cozeloop.log');
670
709
  const hooksJson = path.join(home, 'hooks.json');
671
710
 
672
- // 1. Write Python hook scripts (trace + refresh)
673
- // Token is read from ~/.cozeloop/credentials.json at runtime
711
+ // 1. Write Python hook script
674
712
  ensureDir(hooksDir);
675
713
  writeHookScript(hookScript, readScript('codex/cozeloop_hook.py'));
676
- const refreshScript = path.join(hooksDir, 'cozeloop_refresh.py');
677
- writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
678
714
 
679
715
  // 2. Write env file with chmod 600
680
716
  const envLines = [
@@ -707,14 +743,14 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
707
743
  try {
708
744
  fs.writeFileSync(envFile, envContent, { mode: 0o600 });
709
745
  } catch (e) {
710
- errorBox([`ERROR: Cannot write credentials to ${envFile}`, '', e.message]);
746
+ errorBox([`ERROR: Cannot write hook environment to ${envFile}`, '', e.message]);
711
747
  }
712
- ok(`Credentials written to ${envFile} (chmod 600)`);
748
+ ok(`Hook environment written to ${envFile} (chmod 600)`);
713
749
 
714
- // 3. Merge hooks.json — Stop (trace 收尾) + PostToolUse (trace 增量) + SessionStart (refresh)
750
+ // 3. Merge hooks.json — Stop (trace 收尾) + PostToolUse (trace 增量)
715
751
  // 命令用绝对路径(CODEX_HOME 不一定是 ~/.codex)。
752
+ // 同时清掉旧版本写入的 cozeloop_refresh.py SessionStart hook。
716
753
  const hookCmd = `set -a && . ${envFile} && set +a && ${pythonCmd} ${hookScript}`;
717
- const refreshCmd = `${pythonCmd} ${refreshScript}`;
718
754
 
719
755
  const hooks = mergeJson(hooksJson, (existing) => {
720
756
  if (!existing.hooks) existing.hooks = {};
@@ -734,12 +770,11 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
734
770
  );
735
771
  existing.hooks.PostToolUse.push({ matcher: null, hooks: [{ type: 'command', command: hookCmd, timeout: 60 }] });
736
772
 
737
- // SessionStart hook token refresh
773
+ // Remove legacy token-refresh hook entries.
738
774
  if (!existing.hooks.SessionStart) existing.hooks.SessionStart = [];
739
775
  existing.hooks.SessionStart = existing.hooks.SessionStart.filter(
740
776
  entry => !entry.hooks?.some(h => h.command?.includes('cozeloop_refresh.py'))
741
777
  );
742
- existing.hooks.SessionStart.push({ matcher: null, hooks: [{ type: 'command', command: refreshCmd, timeout: 15 }] });
743
778
 
744
779
  return existing;
745
780
  });
@@ -1063,7 +1098,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force, tokenSourc
1063
1098
  }
1064
1099
  }
1065
1100
 
1066
- // ─── 8. Auth Device Code OAuth + token store ───────────────────────────────
1101
+ // ─── 8. Trace verification HTTP helpers ─────────────────────────────────────
1067
1102
  const https = require('https');
1068
1103
  const crypto = require('crypto');
1069
1104
 
@@ -1377,18 +1412,15 @@ async function verifyTraceReport(token, workspaceId, pairCode, tracesUrl) {
1377
1412
  }
1378
1413
 
1379
1414
  // ── OpenClaw 专属上报链路校验 ──────────────────────────────────────────────
1380
- // 为什么单独一条:claude-code/codex 的 verify 用主流程刚 getValidToken() 刷新过的有效
1415
+ // 为什么单独一条:claude-code/codex 的 verify 用主流程解析到的 token
1381
1416
  // token 直发,而 openclaw 运行时上报用的是【写死在 openclaw.json 插件 config.authorization
1382
1417
  // 里的静态 token】。两者是不同 token —— 插件那个失效(401/4100)时通用 verify 照样 ok,
1383
1418
  // 这就是“verify=ok 但实际查不到 trace”假象的根因。本函数改为读插件实际配置的 token 打
1384
1419
  // ingest,真实反映运行时会不会 401。
1385
1420
  //
1386
1421
  // cloud/local 兼容:插件配置位置都在 resolveHomeDir(cloud)/.openclaw/openclaw.json,
1387
- // endpoint 都走 getOtelEndpointBase(cloud),逻辑统一。差异在 token 刷新:
1388
- // - local:disableLocalCredentials=false,插件会读 ~/.cozeloop/credentials.json 自动刷新,
1389
- // 所以额外检测【实际加载的插件是否含刷新逻辑 getRefreshedToken】,无则告警。
1390
- // - cloud:disableLocalCredentials=true,插件只用写死的 token、不刷新,token 失效需重注入,
1391
- // 刷新能力检测对 cloud 无意义(跳过)。
1422
+ // endpoint 都走 getOtelEndpointBase(cloud),逻辑统一。OpenClaw 插件只使用 onboard 写入
1423
+ // authorization,不再读取或刷新本地 credentials
1392
1424
  async function verifyOpenClawTraceLink(cloud, pairCode) {
1393
1425
  const home = resolveHomeDir(cloud);
1394
1426
  const configPath = path.join(home, '.openclaw', 'openclaw.json');
@@ -1480,60 +1512,14 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
1480
1512
  if (snippet) console.log(snippet);
1481
1513
  // 4100/401 = 该 token 已失效。指出根因与修复方式。
1482
1514
  if (res.status === 401 || /\b4100\b/.test(res.body || '')) {
1483
- info('插件配置的 token 已失效。运行时上报会 401 → OTLP 抛 unhandled rejection → gateway 崩溃 → span 丢失。');
1484
- info('修复:重跑 `node index.js --agent=openclaw --force` 写入新 token(local 会从 ~/.cozeloop 自动刷新)。');
1485
- }
1486
- }
1487
-
1488
- // 2) 仅 local:检测实际加载的插件是否具备 token 自动刷新能力。
1489
- // cloud 主动 disableLocalCredentials,不刷新,检测无意义。
1490
- if (!cloud) {
1491
- const refreshOk = openClawPluginHasRefresh(home);
1492
- if (refreshOk === true) {
1493
- ok('openclaw 插件具备 token 自动刷新能力 (getRefreshedToken)。');
1494
- } else if (refreshOk === false) {
1495
- warn('本机加载的 openclaw 插件【无 token 刷新逻辑】,token 过期后会反复 401 崩 gateway。');
1496
- info('修复:重跑 `node index.js --agent=openclaw --force` 安装带刷新逻辑的新插件。');
1515
+ info('插件配置的 token 已失效。运行时上报会 401span 会丢失。');
1516
+ info('修复:重跑 `npx coze_lab --agent-id=<id> --force` 写入 agent config 中最新 patToken。');
1497
1517
  }
1498
- // refreshOk === null:定位不到插件文件,不下结论(不误报)。
1499
1518
  }
1500
1519
 
1501
1520
  return { success, status: res.status, body: res.body || '' };
1502
1521
  }
1503
1522
 
1504
- // 检测本机【实际加载的】openclaw trace 插件是否含 token 刷新逻辑(getRefreshedToken)。
1505
- // 返回 true=有 / false=无 / null=定位不到插件文件(不下结论)。
1506
- // 探测顺序:openclaw plugins list 给出的真实路径 > onboard 安装位置 ~/.cozeloop/openclaw-plugin
1507
- // > 历史手改位置 ~/.openclaw/workspace/cozeloop-trace-fix。
1508
- function openClawPluginHasRefresh(home) {
1509
- const candidates = [];
1510
- // openclaw plugins list 拿实际加载路径(最准——能发现 cozeloop-trace-fix 这类残留旧插件)
1511
- try {
1512
- const { execSync } = require('child_process');
1513
- const out = execSync('openclaw plugins list', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
1514
- for (const line of out.split(/\r?\n/)) {
1515
- if (!/cozeloop|trace/i.test(line)) continue;
1516
- const m = line.match(/(\/[^\s'"]+)/); // 抓行内绝对路径
1517
- if (m) candidates.push(m[1]);
1518
- }
1519
- } catch { /* CLI 不可用则回退已知路径 */ }
1520
- candidates.push(path.join(home, '.cozeloop', 'openclaw-plugin'));
1521
- candidates.push(path.join(home, '.openclaw', 'workspace', 'cozeloop-trace-fix'));
1522
-
1523
- let foundAny = false;
1524
- for (const base of candidates) {
1525
- for (const rel of ['dist/cozeloop-exporter.js', 'dist/index.js', 'cozeloop-exporter.js', 'index.js']) {
1526
- const f = path.isAbsolute(rel) ? rel : path.join(base, rel);
1527
- try {
1528
- if (!fs.existsSync(f)) continue;
1529
- foundAny = true;
1530
- if (fs.readFileSync(f, 'utf8').includes('getRefreshedToken')) return true;
1531
- } catch { /* ignore */ }
1532
- }
1533
- }
1534
- return foundAny ? false : null;
1535
- }
1536
-
1537
1523
  function httpsGet(url, headers) {
1538
1524
  return new Promise((resolve, reject) => {
1539
1525
  const h = { ...(headers || {}) };
@@ -1547,254 +1533,10 @@ function httpsGet(url, headers) {
1547
1533
  });
1548
1534
  }
1549
1535
 
1550
- // ── Credentials store ──────────────────────────────────────────────────────
1551
- function loadCredentials() {
1552
- try {
1553
- return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'));
1554
- } catch {
1555
- return null;
1556
- }
1557
- }
1558
-
1559
- function saveCredentials(creds) {
1560
- // 0o700:凭证目录仅 owner 可读/进入,其他用户无法枚举 ~/.cozeloop 内容。
1561
- fs.mkdirSync(path.dirname(CREDS_PATH), { recursive: true, mode: 0o700 });
1562
- atomicWriteFileSync(CREDS_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 });
1563
- }
1564
-
1565
- function deleteCredentials() {
1566
- try { fs.unlinkSync(CREDS_PATH); } catch { /* already gone */ }
1567
- }
1568
-
1569
- function isExpired(creds) {
1570
- if (!creds || !creds.expires_at) return true;
1571
- return Date.now() >= creds.expires_at - REFRESH_THRESHOLD_MS;
1572
- }
1573
-
1574
- // ── Refresh token ──────────────────────────────────────────────────────────
1575
- async function refreshToken(creds) {
1576
- info('Access token expiring soon, refreshing...');
1577
- let res;
1578
- try {
1579
- res = await httpsPost(`${COZE_API}/api/permission/oauth2/token`, {
1580
- grant_type: 'refresh_token',
1581
- client_id: CLIENT_ID,
1582
- refresh_token: creds.refresh_token,
1583
- });
1584
- } catch (e) {
1585
- errorBox(['ERROR: Could not refresh token', '', e.message]);
1586
- }
1587
-
1588
- let data;
1589
- try { data = JSON.parse(res.body); } catch {
1590
- errorBox(['ERROR: Unexpected response while refreshing token']);
1591
- }
1592
-
1593
- if (data.error || res.status !== 200) {
1594
- warn(`Token refresh failed (${data.error || res.status}), re-authorizing...`);
1595
- return null; // caller will re-run device code flow
1596
- }
1597
-
1598
- const updated = {
1599
- access_token: data.access_token,
1600
- refresh_token: data.refresh_token ?? creds.refresh_token,
1601
- expires_at: (data.expires_in ?? 0) * 1000, // expires_at stored in milliseconds (Python 端按毫秒读)
1602
- workspace_id: creds.workspace_id ?? WORKSPACE_ID, // preserve workspace_id
1603
- };
1604
- saveCredentials(updated);
1605
- ok('Token refreshed successfully.');
1606
- return updated;
1607
- }
1608
-
1609
- // ── Device Code flow ───────────────────────────────────────────────────────
1610
- async function deviceCodeAuth() {
1611
- // Step 1: Get device code
1612
- let res;
1613
- try {
1614
- res = await httpsPost(`${COZE_API}/api/permission/oauth2/device/code`, {
1615
- client_id: CLIENT_ID,
1616
- });
1617
- } catch (e) {
1618
- errorBox(['ERROR: Could not reach Coze API', '', e.message]);
1619
- }
1620
-
1621
- let data;
1622
- try { data = JSON.parse(res.body); } catch {
1623
- errorBox(['ERROR: Unexpected response from Coze API']);
1624
- }
1625
-
1626
- if (res.status !== 200 || data.error) {
1627
- errorBox([
1628
- 'ERROR: Failed to start device authorization',
1629
- '',
1630
- data.error_description || data.error || `HTTP ${res.status}`,
1631
- ]);
1632
- }
1633
-
1634
- const { device_code, user_code, verification_uri, expires_in, interval = 5 } = data;
1635
- const activation_url = `${verification_uri}?user_code=${encodeURIComponent(user_code)}`;
1636
-
1637
- console.log('');
1638
- box([
1639
- ' 请在浏览器中打开以下链接完成授权:',
1640
- '',
1641
- ` ${activation_url}`,
1642
- '',
1643
- ` 验证码将在 ${expires_in} 秒后过期`,
1644
- ], C.cyan);
1645
- console.log('');
1646
-
1647
- // Try to open browser automatically
1648
- try {
1649
- const { execSync } = require('child_process');
1650
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1651
- execSync(`${opener} "${activation_url}"`, { stdio: 'ignore' });
1652
- info('已自动打开浏览器,请在浏览器中完成授权...');
1653
- } catch {
1654
- info('请手动在浏览器中打开上方链接完成授权...');
1655
- }
1656
-
1657
- // Step 2: Poll for token
1658
- const deadline = Date.now() + expires_in * 1000;
1659
- let pollInterval = interval * 1000;
1660
-
1661
- process.stdout.write(`${C.cyan}[i]${C.reset} 等待授权中`);
1662
-
1663
- while (Date.now() < deadline) {
1664
- await new Promise(r => setTimeout(r, pollInterval));
1665
- process.stdout.write('.');
1666
-
1667
- let pollRes;
1668
- try {
1669
- pollRes = await httpsPost(`${COZE_API}/api/permission/oauth2/token`, {
1670
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
1671
- client_id: CLIENT_ID,
1672
- device_code,
1673
- });
1674
- } catch { continue; }
1675
-
1676
- let pollData;
1677
- try { pollData = JSON.parse(pollRes.body); } catch { continue; }
1678
-
1679
- if (pollData.access_token) {
1680
- process.stdout.write('\n');
1681
- const creds = {
1682
- access_token: pollData.access_token,
1683
- refresh_token: pollData.refresh_token,
1684
- expires_at: (pollData.expires_in ?? 0) * 1000, // expires_at stored in milliseconds (Python 端按毫秒读)
1685
- workspace_id: WORKSPACE_ID,
1686
- };
1687
- saveCredentials(creds);
1688
- console.log('');
1689
- ok('授权成功!Token 已保存到 ~/.cozeloop/credentials.json');
1690
- return creds;
1691
- }
1692
-
1693
- if (pollData.error === 'slow_down') {
1694
- pollInterval += 5000;
1695
- } else if (pollData.error === 'access_denied') {
1696
- process.stdout.write('\n');
1697
- errorBox(['ERROR: 用户拒绝了授权请求']);
1698
- } else if (pollData.error === 'expired_token') {
1699
- process.stdout.write('\n');
1700
- errorBox(['ERROR: 验证码已过期,请重新运行命令']);
1701
- }
1702
- // authorization_pending → keep polling
1703
- }
1704
-
1705
- process.stdout.write('\n');
1706
- errorBox(['ERROR: 授权超时,请重新运行命令']);
1707
- }
1708
-
1709
- // ── Get valid token (load → refresh → re-auth) ────────────────────────────
1710
- async function getValidToken() {
1711
- let creds = loadCredentials();
1712
-
1713
- if (creds && !isExpired(creds)) {
1714
- ok('已找到有效的本地授权,跳过授权步骤。');
1715
- return creds.access_token;
1716
- }
1717
-
1718
- if (creds && creds.refresh_token) {
1719
- const refreshed = await refreshToken(creds);
1720
- if (refreshed) return refreshed.access_token;
1721
- }
1722
-
1723
- // Need fresh authorization
1724
- info('未找到本地授权,启动 Device Code 授权流程...');
1725
- const fresh = await deviceCodeAuth();
1726
- return fresh.access_token;
1727
- }
1728
-
1729
- // ── Logout ────────────────────────────────────────────────────────────────
1730
- function logout() {
1731
- const creds = loadCredentials();
1732
- if (!creds) {
1733
- info('本地没有保存的授权信息,无需退出。');
1734
- return;
1735
- }
1736
- deleteCredentials();
1737
- successBox(['已成功退出登录', '', '本地 Token 已清除 (~/.cozeloop/credentials.json)']);
1738
- }
1739
-
1740
- // ── Auth status ───────────────────────────────────────────────────────────
1741
- function authStatus() {
1742
- const creds = loadCredentials();
1743
- if (!creds) {
1744
- box([
1745
- 'Auth Status: NOT LOGGED IN',
1746
- '',
1747
- '~/.cozeloop/credentials.json not found.',
1748
- 'Run with --agent=<type> to authorize.',
1749
- ], C.yellow);
1750
- return;
1751
- }
1752
-
1753
- const expiresAt = creds.expires_at ? new Date(creds.expires_at) : null;
1754
- const now = Date.now();
1755
- const remainMs = creds.expires_at ? creds.expires_at - now : null;
1756
- const remainMin = remainMs != null ? Math.floor(remainMs / 60000) : null;
1757
-
1758
- function formatRemaining(ms) {
1759
- if (ms == null) return 'unknown';
1760
- const min = Math.floor(ms / 60000);
1761
- if (min < 60) return `${min} 分钟`;
1762
- const hr = Math.floor(min / 60);
1763
- if (hr < 24) return `${hr} 小时`;
1764
- const day = Math.floor(hr / 24);
1765
- if (day < 365) return `${day} 天`;
1766
- return `${Math.floor(day / 365)} 年`;
1767
- }
1768
-
1769
- let statusLabel, statusColor;
1770
- if (!expiresAt) {
1771
- statusLabel = 'UNKNOWN (no expiry info)';
1772
- statusColor = C.yellow;
1773
- } else if (remainMs < 0) {
1774
- statusLabel = 'EXPIRED';
1775
- statusColor = C.red;
1776
- } else if (remainMs < REFRESH_THRESHOLD_MS) {
1777
- statusLabel = `EXPIRING SOON (剩余 ${formatRemaining(remainMs)})`;
1778
- statusColor = C.yellow;
1779
- } else {
1780
- statusLabel = `VALID (剩余 ${formatRemaining(remainMs)})`;
1781
- statusColor = C.green;
1782
- }
1783
-
1784
- const lines = [
1785
- `Auth Status: ${statusLabel}`,
1786
- '',
1787
- `Token: ${creds.access_token ? creds.access_token.slice(0, 20) + '...' : 'n/a'}`,
1788
- `Expires at: ${expiresAt ? expiresAt.toLocaleString() : 'unknown'}`,
1789
- `Refresh: ${creds.refresh_token ? 'available' : 'not available'}`,
1790
- ];
1791
- box(lines, statusColor);
1792
- }
1793
-
1794
1536
  // ─── 9. Main ─────────────────────────────────────────────────────────────────
1795
1537
  const NEXT_STEP = {
1796
1538
  'claude-code': 'Hook 已写入。Claude Code 会自动热重载 hooks,当前会话即刻生效,无需 /new 或重启。',
1797
- 'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex 会话(已配置 SessionStart hook,新会话自动加载)。',
1539
+ 'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex 会话。',
1798
1540
  };
1799
1541
 
1800
1542
  function openClawNextStep(written) {
@@ -1814,64 +1556,31 @@ async function main() {
1814
1556
 
1815
1557
  const args = validateArgs(parseArgs());
1816
1558
 
1817
- // ── Logout mode ──────────────────────────────────────────────────────────
1818
- if (args.logout) {
1819
- logout();
1820
- return;
1821
- }
1822
-
1823
- if (args.status) {
1824
- authStatus();
1825
- return;
1826
- }
1827
-
1828
- // ── Refresh mode ─────────────────────────────────────────────────────────
1829
- // ── Login mode ───────────────────────────────────────────────────────────
1830
- if (args.login) {
1831
- const existing = loadCredentials();
1832
- if (existing && !isExpired(existing)) {
1833
- warnBox([
1834
- '当前已有有效的登录凭证。',
1559
+ if (args.verify) {
1560
+ info('验证 trace 上报链路...');
1561
+ let token = '';
1562
+ let tokenSource = '';
1563
+ if (args.cloud) {
1564
+ const tokenInfo = getCloudTokenInfo();
1565
+ token = tokenInfo.token;
1566
+ tokenSource = tokenInfo.source;
1567
+ if (!token) {
1568
+ errorBox([
1569
+ 'ERROR: verify 需要环境变量 COZELOOP_API_TOKEN 或 COZE_API_TOKEN',
1570
+ '',
1571
+ 'coze_lab 不再读取本地 OAuth credentials。',
1572
+ ]);
1573
+ }
1574
+ } else if (args.agentId && args.patToken) {
1575
+ token = args.patToken;
1576
+ tokenSource = TOKEN_SOURCE_AGENT_PAT;
1577
+ } else {
1578
+ errorBox([
1579
+ 'ERROR: 本地 verify 需要 --agent-id 且 config.json 中存在 patToken',
1835
1580
  '',
1836
- '如需重新登录,请先运行 --logout,或使用 --refresh 刷新 Token。',
1581
+ '请确认 ~/.coze/agents/<id>/config.json 包含 patToken 后重试。',
1837
1582
  ]);
1838
- return;
1839
1583
  }
1840
- deleteCredentials();
1841
- await deviceCodeAuth();
1842
- return;
1843
- }
1844
-
1845
- if (args.refresh) {
1846
- const creds = loadCredentials();
1847
- if (!creds) {
1848
- box([
1849
- 'ERROR: 未找到本地授权信息',
1850
- '',
1851
- '请先运行 --agent=<type> 完成授权。',
1852
- ], C.red);
1853
- process.exit(1);
1854
- }
1855
- if (!creds.refresh_token) {
1856
- box([
1857
- 'ERROR: 当前凭证没有 refresh_token',
1858
- '',
1859
- '请运行 --logout 后重新 --agent=<type> 授权。',
1860
- ], C.red);
1861
- process.exit(1);
1862
- }
1863
- const refreshed = await refreshToken(creds);
1864
- if (!refreshed) {
1865
- warn('Refresh token 已失效,启动 Device Code 重新授权...');
1866
- console.log('');
1867
- await deviceCodeAuth();
1868
- }
1869
- return;
1870
- }
1871
-
1872
- if (args.verify) {
1873
- info('验证 trace 上报链路...');
1874
- const token = await getValidToken(); // 无凭证会自动走登录/刷新
1875
1584
  console.log('');
1876
1585
  const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
1877
1586
  // 若本机装了 openclaw 插件,额外校验插件【实际配置的静态 token】——通用 verify 用刚刷新的
@@ -1884,7 +1593,7 @@ async function main() {
1884
1593
  if (cfg?.plugins?.entries?.['openclaw-cozeloop-trace']?.config?.authorization) {
1885
1594
  console.log('');
1886
1595
  info('检测到 openclaw cozeloop-trace 插件,校验其实际 token...');
1887
- const ocRes = await verifyOpenClawTraceLink(false, args.pairCode);
1596
+ const ocRes = await verifyOpenClawTraceLink(args.cloud, args.pairCode);
1888
1597
  ocOk = ocRes.success;
1889
1598
  }
1890
1599
  } catch { /* 读不了就跳过 openclaw 校验 */ }
@@ -1906,11 +1615,10 @@ async function main() {
1906
1615
  console.log('');
1907
1616
  }
1908
1617
 
1909
- // Step 1: Authorize.
1910
- // 云端模式:token 取自 sandbox 注入的环境变量,跳过 OAuth / credentials.json。
1618
+ // Step 1: Resolve trace token.
1619
+ // 云端模式:token 取自 sandbox 注入的环境变量。
1911
1620
  // 优先使用 COZELOOP_API_TOKEN;兼容使用 COZE_API_TOKEN,并以真实 selfcheck 为准。
1912
- // 本地 --agent-id config.patToken 存在:优先使用该 PAT,跳过 OAuth / credentials.json
1913
- // 其它本地模式:load cached → refresh → device code。
1621
+ // 本地模式:必须通过 --agent-id 读取 config.patToken。
1914
1622
  // 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
1915
1623
  let token;
1916
1624
  let tokenSource = '';
@@ -1933,14 +1641,20 @@ async function main() {
1933
1641
  info('将兼容使用 COZE_API_TOKEN 作为 trace token,并通过 selfcheck 验证实际可用性。');
1934
1642
  }
1935
1643
  } else {
1936
- info('Step 1/5: 检查授权状态...');
1644
+ info('Step 1/5: 从 agent config 读取 patToken...');
1937
1645
  if (args.agentId && args.patToken) {
1938
1646
  token = args.patToken;
1939
1647
  tokenSource = TOKEN_SOURCE_AGENT_PAT;
1940
- ok(`已从 ~/.coze/agents/${args.agentId}/config.json 读取 patToken,跳过本地授权。`);
1648
+ ok(`已从 ~/.coze/agents/${args.agentId}/config.json 读取 patToken。`);
1941
1649
  } else {
1942
- token = await getValidToken();
1943
- tokenSource = 'credentials';
1650
+ errorBox([
1651
+ 'ERROR: 本地模式要求 ~/.coze/agents/<agentId>/config.json 中存在 patToken',
1652
+ '',
1653
+ 'coze_lab 不再提供 Device Code / OAuth 本地授权兜底。',
1654
+ args.agentId
1655
+ ? `请确认 ~/.coze/agents/${args.agentId}/config.json 包含 patToken 后重试。`
1656
+ : '请使用 --agent-id=<id> 运行,以便读取对应 agent config。',
1657
+ ]);
1944
1658
  }
1945
1659
  }
1946
1660
  console.log('');
@@ -2025,7 +1739,7 @@ async function main() {
2025
1739
 
2026
1740
  // Step 5: Verify trace reporting end-to-end
2027
1741
  info('Step 5/5: 验证 trace 上报链路...');
2028
- // openclaw 走专属校验:claude-code/codex 的 verify 用主流程刚 getValidToken() 刷新过的
1742
+ // openclaw 走专属校验:claude-code/codex 的 verify 用主流程解析到的
2029
1743
  // 有效 token 直发,测不到 openclaw 插件【写死在 openclaw.json 里的静态 token】是否失效
2030
1744
  // (插件不读这个临时 token)。openclaw 必须用插件实际配置的 authorization 打 ingest,
2031
1745
  // 才能真实反映运行时上报会不会 401。cloud/local 配置位置一致,统一走这条。
@@ -2089,5 +1803,7 @@ module.exports = {
2089
1803
  getAgentPatToken,
2090
1804
  mergeJson,
2091
1805
  atomicWriteFileSync,
2092
- isExpired,
1806
+ VERSION_WHITELIST,
1807
+ VERSION_SUPPORT,
1808
+ isVersionAtLeast,
2093
1809
  };