coze_lab 0.1.44 → 0.1.45
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 +48 -58
- package/index.js +95 -405
- package/package.json +1 -2
- package/scripts/claude-code/cozeloop_hook.py +6 -244
- package/scripts/codex/cozeloop_hook.py +7 -371
- package/scripts/openclaw/dist/cozeloop-exporter.js +11 -183
- package/scripts/openclaw/dist/index.js +1 -1
- package/scripts/openclaw/openclaw.plugin.json +2 -2
- package/scripts/openclaw/package.json +1 -1
- package/scripts/shared/cozeloop_refresh.py +0 -57
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
|
|
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
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
286
|
-
' --agent-
|
|
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
|
-
|
|
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 ──────────────────────────────────────────────────────
|
|
@@ -545,15 +565,13 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
|
|
|
545
565
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
546
566
|
const localSettingsPath = path.join(baseDir, '.claude', 'settings.local.json');
|
|
547
567
|
|
|
548
|
-
// 1. Write Python hook
|
|
568
|
+
// 1. Write Python hook script — 脚本放全局 ~/.claude/hooks,可共享
|
|
549
569
|
ensureDir(hooksDir);
|
|
550
570
|
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
571
|
|
|
554
|
-
// 2. Merge settings.json — Stop (trace 收尾) + PostToolUse (trace 增量)
|
|
572
|
+
// 2. Merge settings.json — Stop (trace 收尾) + PostToolUse (trace 增量)。用绝对路径。
|
|
573
|
+
// 同时清掉旧版本写入的 cozeloop_refresh.py hook,避免继续走 OAuth refresh 兜底。
|
|
555
574
|
const hookCmd = `${pythonCmd} ${hookScript}`;
|
|
556
|
-
const refreshCmd = `${pythonCmd} ${refreshScript}`;
|
|
557
575
|
|
|
558
576
|
ensureDir(claudeDir);
|
|
559
577
|
let settings;
|
|
@@ -576,12 +594,11 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
|
|
|
576
594
|
);
|
|
577
595
|
existing.hooks.PostToolUse.push({ matcher: '', hooks: [{ type: 'command', command: hookCmd }] });
|
|
578
596
|
|
|
579
|
-
//
|
|
597
|
+
// Remove legacy token-refresh hook entries.
|
|
580
598
|
if (!existing.hooks.UserPromptSubmit) existing.hooks.UserPromptSubmit = [];
|
|
581
599
|
existing.hooks.UserPromptSubmit = existing.hooks.UserPromptSubmit.filter(
|
|
582
600
|
entry => !entry.hooks?.some(h => h.command?.includes('cozeloop_refresh.py'))
|
|
583
601
|
);
|
|
584
|
-
existing.hooks.UserPromptSubmit.push({ matcher: '', hooks: [{ type: 'command', command: refreshCmd }] });
|
|
585
602
|
|
|
586
603
|
return existing;
|
|
587
604
|
});
|
|
@@ -596,7 +613,7 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
|
|
|
596
613
|
}
|
|
597
614
|
ok(`Hook registered in ${settingsPath}`);
|
|
598
615
|
|
|
599
|
-
// 3. Write
|
|
616
|
+
// 3. Write hook environment into <baseDir>/.claude/settings.local.json
|
|
600
617
|
ensureDir(path.join(baseDir, '.claude'));
|
|
601
618
|
ensureDir(path.dirname(logFile));
|
|
602
619
|
let localSettings;
|
|
@@ -648,9 +665,9 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
|
|
|
648
665
|
try {
|
|
649
666
|
atomicWriteFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2));
|
|
650
667
|
} catch (e) {
|
|
651
|
-
errorBox([`ERROR: Cannot write
|
|
668
|
+
errorBox([`ERROR: Cannot write hook environment to ${localSettingsPath}`, '', e.message]);
|
|
652
669
|
}
|
|
653
|
-
ok(`
|
|
670
|
+
ok(`Hook environment written to ${localSettingsPath}`);
|
|
654
671
|
|
|
655
672
|
|
|
656
673
|
return { hookScript, settingsPath, localSettingsPath, logFile };
|
|
@@ -658,8 +675,7 @@ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud
|
|
|
658
675
|
|
|
659
676
|
// writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
|
|
660
677
|
// 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
|
|
661
|
-
//
|
|
662
|
-
// 本地 --agent-id 且 config.patToken 存在时写入该 PAT,并用 COZELOOP_TOKEN_SOURCE 标记来源。
|
|
678
|
+
// 本地 --agent-id 从 config.patToken 写入 token,并用 COZELOOP_TOKEN_SOURCE 标记来源。
|
|
663
679
|
// cloud=true 时写 COZELAB_ONBOARD_CLOUD,并带入 sandbox 注入的 trace token。
|
|
664
680
|
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSource) {
|
|
665
681
|
const home = codexHome || path.join(os.homedir(), '.codex');
|
|
@@ -669,12 +685,9 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
|
|
|
669
685
|
const logFile = path.join(hooksDir, 'cozeloop.log');
|
|
670
686
|
const hooksJson = path.join(home, 'hooks.json');
|
|
671
687
|
|
|
672
|
-
// 1. Write Python hook
|
|
673
|
-
// Token is read from ~/.cozeloop/credentials.json at runtime
|
|
688
|
+
// 1. Write Python hook script
|
|
674
689
|
ensureDir(hooksDir);
|
|
675
690
|
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
691
|
|
|
679
692
|
// 2. Write env file with chmod 600
|
|
680
693
|
const envLines = [
|
|
@@ -707,14 +720,14 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
|
|
|
707
720
|
try {
|
|
708
721
|
fs.writeFileSync(envFile, envContent, { mode: 0o600 });
|
|
709
722
|
} catch (e) {
|
|
710
|
-
errorBox([`ERROR: Cannot write
|
|
723
|
+
errorBox([`ERROR: Cannot write hook environment to ${envFile}`, '', e.message]);
|
|
711
724
|
}
|
|
712
|
-
ok(`
|
|
725
|
+
ok(`Hook environment written to ${envFile} (chmod 600)`);
|
|
713
726
|
|
|
714
|
-
// 3. Merge hooks.json — Stop (trace 收尾) + PostToolUse (trace 增量)
|
|
727
|
+
// 3. Merge hooks.json — Stop (trace 收尾) + PostToolUse (trace 增量)
|
|
715
728
|
// 命令用绝对路径(CODEX_HOME 不一定是 ~/.codex)。
|
|
729
|
+
// 同时清掉旧版本写入的 cozeloop_refresh.py SessionStart hook。
|
|
716
730
|
const hookCmd = `set -a && . ${envFile} && set +a && ${pythonCmd} ${hookScript}`;
|
|
717
|
-
const refreshCmd = `${pythonCmd} ${refreshScript}`;
|
|
718
731
|
|
|
719
732
|
const hooks = mergeJson(hooksJson, (existing) => {
|
|
720
733
|
if (!existing.hooks) existing.hooks = {};
|
|
@@ -734,12 +747,11 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSo
|
|
|
734
747
|
);
|
|
735
748
|
existing.hooks.PostToolUse.push({ matcher: null, hooks: [{ type: 'command', command: hookCmd, timeout: 60 }] });
|
|
736
749
|
|
|
737
|
-
//
|
|
750
|
+
// Remove legacy token-refresh hook entries.
|
|
738
751
|
if (!existing.hooks.SessionStart) existing.hooks.SessionStart = [];
|
|
739
752
|
existing.hooks.SessionStart = existing.hooks.SessionStart.filter(
|
|
740
753
|
entry => !entry.hooks?.some(h => h.command?.includes('cozeloop_refresh.py'))
|
|
741
754
|
);
|
|
742
|
-
existing.hooks.SessionStart.push({ matcher: null, hooks: [{ type: 'command', command: refreshCmd, timeout: 15 }] });
|
|
743
755
|
|
|
744
756
|
return existing;
|
|
745
757
|
});
|
|
@@ -1063,7 +1075,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force, tokenSourc
|
|
|
1063
1075
|
}
|
|
1064
1076
|
}
|
|
1065
1077
|
|
|
1066
|
-
// ─── 8.
|
|
1078
|
+
// ─── 8. Trace verification HTTP helpers ─────────────────────────────────────
|
|
1067
1079
|
const https = require('https');
|
|
1068
1080
|
const crypto = require('crypto');
|
|
1069
1081
|
|
|
@@ -1377,18 +1389,15 @@ async function verifyTraceReport(token, workspaceId, pairCode, tracesUrl) {
|
|
|
1377
1389
|
}
|
|
1378
1390
|
|
|
1379
1391
|
// ── OpenClaw 专属上报链路校验 ──────────────────────────────────────────────
|
|
1380
|
-
// 为什么单独一条:claude-code/codex 的 verify
|
|
1392
|
+
// 为什么单独一条:claude-code/codex 的 verify 用主流程解析到的 token
|
|
1381
1393
|
// token 直发,而 openclaw 运行时上报用的是【写死在 openclaw.json 插件 config.authorization
|
|
1382
1394
|
// 里的静态 token】。两者是不同 token —— 插件那个失效(401/4100)时通用 verify 照样 ok,
|
|
1383
1395
|
// 这就是“verify=ok 但实际查不到 trace”假象的根因。本函数改为读插件实际配置的 token 打
|
|
1384
1396
|
// ingest,真实反映运行时会不会 401。
|
|
1385
1397
|
//
|
|
1386
1398
|
// cloud/local 兼容:插件配置位置都在 resolveHomeDir(cloud)/.openclaw/openclaw.json,
|
|
1387
|
-
// endpoint 都走 getOtelEndpointBase(cloud)
|
|
1388
|
-
//
|
|
1389
|
-
// 所以额外检测【实际加载的插件是否含刷新逻辑 getRefreshedToken】,无则告警。
|
|
1390
|
-
// - cloud:disableLocalCredentials=true,插件只用写死的 token、不刷新,token 失效需重注入,
|
|
1391
|
-
// 刷新能力检测对 cloud 无意义(跳过)。
|
|
1399
|
+
// endpoint 都走 getOtelEndpointBase(cloud),逻辑统一。OpenClaw 插件只使用 onboard 写入
|
|
1400
|
+
// 的 authorization,不再读取或刷新本地 credentials。
|
|
1392
1401
|
async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
1393
1402
|
const home = resolveHomeDir(cloud);
|
|
1394
1403
|
const configPath = path.join(home, '.openclaw', 'openclaw.json');
|
|
@@ -1480,60 +1489,14 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
|
1480
1489
|
if (snippet) console.log(snippet);
|
|
1481
1490
|
// 4100/401 = 该 token 已失效。指出根因与修复方式。
|
|
1482
1491
|
if (res.status === 401 || /\b4100\b/.test(res.body || '')) {
|
|
1483
|
-
info('插件配置的 token 已失效。运行时上报会 401
|
|
1484
|
-
info('修复:重跑 `
|
|
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` 安装带刷新逻辑的新插件。');
|
|
1492
|
+
info('插件配置的 token 已失效。运行时上报会 401,span 会丢失。');
|
|
1493
|
+
info('修复:重跑 `npx coze_lab --agent-id=<id> --force` 写入 agent config 中最新 patToken。');
|
|
1497
1494
|
}
|
|
1498
|
-
// refreshOk === null:定位不到插件文件,不下结论(不误报)。
|
|
1499
1495
|
}
|
|
1500
1496
|
|
|
1501
1497
|
return { success, status: res.status, body: res.body || '' };
|
|
1502
1498
|
}
|
|
1503
1499
|
|
|
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
1500
|
function httpsGet(url, headers) {
|
|
1538
1501
|
return new Promise((resolve, reject) => {
|
|
1539
1502
|
const h = { ...(headers || {}) };
|
|
@@ -1547,254 +1510,10 @@ function httpsGet(url, headers) {
|
|
|
1547
1510
|
});
|
|
1548
1511
|
}
|
|
1549
1512
|
|
|
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
1513
|
// ─── 9. Main ─────────────────────────────────────────────────────────────────
|
|
1795
1514
|
const NEXT_STEP = {
|
|
1796
1515
|
'claude-code': 'Hook 已写入。Claude Code 会自动热重载 hooks,当前会话即刻生效,无需 /new 或重启。',
|
|
1797
|
-
'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex
|
|
1516
|
+
'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex 会话。',
|
|
1798
1517
|
};
|
|
1799
1518
|
|
|
1800
1519
|
function openClawNextStep(written) {
|
|
@@ -1814,64 +1533,31 @@ async function main() {
|
|
|
1814
1533
|
|
|
1815
1534
|
const args = validateArgs(parseArgs());
|
|
1816
1535
|
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if (
|
|
1833
|
-
|
|
1834
|
-
|
|
1536
|
+
if (args.verify) {
|
|
1537
|
+
info('验证 trace 上报链路...');
|
|
1538
|
+
let token = '';
|
|
1539
|
+
let tokenSource = '';
|
|
1540
|
+
if (args.cloud) {
|
|
1541
|
+
const tokenInfo = getCloudTokenInfo();
|
|
1542
|
+
token = tokenInfo.token;
|
|
1543
|
+
tokenSource = tokenInfo.source;
|
|
1544
|
+
if (!token) {
|
|
1545
|
+
errorBox([
|
|
1546
|
+
'ERROR: verify 需要环境变量 COZELOOP_API_TOKEN 或 COZE_API_TOKEN',
|
|
1547
|
+
'',
|
|
1548
|
+
'coze_lab 不再读取本地 OAuth credentials。',
|
|
1549
|
+
]);
|
|
1550
|
+
}
|
|
1551
|
+
} else if (args.agentId && args.patToken) {
|
|
1552
|
+
token = args.patToken;
|
|
1553
|
+
tokenSource = TOKEN_SOURCE_AGENT_PAT;
|
|
1554
|
+
} else {
|
|
1555
|
+
errorBox([
|
|
1556
|
+
'ERROR: 本地 verify 需要 --agent-id 且 config.json 中存在 patToken',
|
|
1835
1557
|
'',
|
|
1836
|
-
'
|
|
1558
|
+
'请确认 ~/.coze/agents/<id>/config.json 包含 patToken 后重试。',
|
|
1837
1559
|
]);
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
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
1560
|
}
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
if (args.verify) {
|
|
1873
|
-
info('验证 trace 上报链路...');
|
|
1874
|
-
const token = await getValidToken(); // 无凭证会自动走登录/刷新
|
|
1875
1561
|
console.log('');
|
|
1876
1562
|
const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
|
|
1877
1563
|
// 若本机装了 openclaw 插件,额外校验插件【实际配置的静态 token】——通用 verify 用刚刷新的
|
|
@@ -1884,7 +1570,7 @@ async function main() {
|
|
|
1884
1570
|
if (cfg?.plugins?.entries?.['openclaw-cozeloop-trace']?.config?.authorization) {
|
|
1885
1571
|
console.log('');
|
|
1886
1572
|
info('检测到 openclaw cozeloop-trace 插件,校验其实际 token...');
|
|
1887
|
-
const ocRes = await verifyOpenClawTraceLink(
|
|
1573
|
+
const ocRes = await verifyOpenClawTraceLink(args.cloud, args.pairCode);
|
|
1888
1574
|
ocOk = ocRes.success;
|
|
1889
1575
|
}
|
|
1890
1576
|
} catch { /* 读不了就跳过 openclaw 校验 */ }
|
|
@@ -1906,11 +1592,10 @@ async function main() {
|
|
|
1906
1592
|
console.log('');
|
|
1907
1593
|
}
|
|
1908
1594
|
|
|
1909
|
-
// Step 1:
|
|
1910
|
-
// 云端模式:token 取自 sandbox
|
|
1595
|
+
// Step 1: Resolve trace token.
|
|
1596
|
+
// 云端模式:token 取自 sandbox 注入的环境变量。
|
|
1911
1597
|
// 优先使用 COZELOOP_API_TOKEN;兼容使用 COZE_API_TOKEN,并以真实 selfcheck 为准。
|
|
1912
|
-
//
|
|
1913
|
-
// 其它本地模式:load cached → refresh → device code。
|
|
1598
|
+
// 本地模式:必须通过 --agent-id 读取 config.patToken。
|
|
1914
1599
|
// 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
|
|
1915
1600
|
let token;
|
|
1916
1601
|
let tokenSource = '';
|
|
@@ -1933,14 +1618,20 @@ async function main() {
|
|
|
1933
1618
|
info('将兼容使用 COZE_API_TOKEN 作为 trace token,并通过 selfcheck 验证实际可用性。');
|
|
1934
1619
|
}
|
|
1935
1620
|
} else {
|
|
1936
|
-
info('Step 1/5:
|
|
1621
|
+
info('Step 1/5: 从 agent config 读取 patToken...');
|
|
1937
1622
|
if (args.agentId && args.patToken) {
|
|
1938
1623
|
token = args.patToken;
|
|
1939
1624
|
tokenSource = TOKEN_SOURCE_AGENT_PAT;
|
|
1940
|
-
ok(`已从 ~/.coze/agents/${args.agentId}/config.json 读取 patToken
|
|
1625
|
+
ok(`已从 ~/.coze/agents/${args.agentId}/config.json 读取 patToken。`);
|
|
1941
1626
|
} else {
|
|
1942
|
-
|
|
1943
|
-
|
|
1627
|
+
errorBox([
|
|
1628
|
+
'ERROR: 本地模式要求 ~/.coze/agents/<agentId>/config.json 中存在 patToken',
|
|
1629
|
+
'',
|
|
1630
|
+
'coze_lab 不再提供 Device Code / OAuth 本地授权兜底。',
|
|
1631
|
+
args.agentId
|
|
1632
|
+
? `请确认 ~/.coze/agents/${args.agentId}/config.json 包含 patToken 后重试。`
|
|
1633
|
+
: '请使用 --agent-id=<id> 运行,以便读取对应 agent config。',
|
|
1634
|
+
]);
|
|
1944
1635
|
}
|
|
1945
1636
|
}
|
|
1946
1637
|
console.log('');
|
|
@@ -2025,7 +1716,7 @@ async function main() {
|
|
|
2025
1716
|
|
|
2026
1717
|
// Step 5: Verify trace reporting end-to-end
|
|
2027
1718
|
info('Step 5/5: 验证 trace 上报链路...');
|
|
2028
|
-
// openclaw 走专属校验:claude-code/codex 的 verify
|
|
1719
|
+
// openclaw 走专属校验:claude-code/codex 的 verify 用主流程解析到的
|
|
2029
1720
|
// 有效 token 直发,测不到 openclaw 插件【写死在 openclaw.json 里的静态 token】是否失效
|
|
2030
1721
|
// (插件不读这个临时 token)。openclaw 必须用插件实际配置的 authorization 打 ingest,
|
|
2031
1722
|
// 才能真实反映运行时上报会不会 401。cloud/local 配置位置一致,统一走这条。
|
|
@@ -2089,5 +1780,4 @@ module.exports = {
|
|
|
2089
1780
|
getAgentPatToken,
|
|
2090
1781
|
mergeJson,
|
|
2091
1782
|
atomicWriteFileSync,
|
|
2092
|
-
isExpired,
|
|
2093
1783
|
};
|