coze_lab 0.1.43 → 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 -54
- package/index.js +135 -414
- package/package.json +1 -2
- package/scripts/claude-code/cozeloop_hook.py +6 -239
- package/scripts/codex/cozeloop_hook.py +7 -366
- package/scripts/openclaw/dist/cozeloop-exporter.js +13 -182
- package/scripts/openclaw/dist/index.js +2 -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,10 @@
|
|
|
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
|
-
|
|
11
|
-
const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
|
|
8
|
+
const TOKEN_SOURCE_AGENT_PAT = 'agent_config.patToken';
|
|
12
9
|
|
|
13
10
|
// ─── 1. Cloud structured output ──────────────────────────────────────────────
|
|
14
11
|
// 云端模式:在 stdout 输出一行机器可读结果 COZE_LAB_RESULT={...},
|
|
@@ -117,7 +114,7 @@ const OPENCLAW_PLUGIN_VERSION = (() => {
|
|
|
117
114
|
}
|
|
118
115
|
})();
|
|
119
116
|
|
|
120
|
-
// Read a single script file
|
|
117
|
+
// Read a single script file as a UTF-8 string.
|
|
121
118
|
function readScript(relPath) {
|
|
122
119
|
return require('fs').readFileSync(require('path').join(SCRIPTS_DIR, relPath), 'utf8');
|
|
123
120
|
}
|
|
@@ -146,6 +143,8 @@ function loadOpenclawFiles() {
|
|
|
146
143
|
function parseArgs() {
|
|
147
144
|
const args = {};
|
|
148
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.
|
|
149
148
|
if (arg === '--logout') { args['logout'] = true; continue; }
|
|
150
149
|
if (arg === '--login') { args['login'] = true; continue; }
|
|
151
150
|
if (arg === '--status') { args['status'] = true; continue; }
|
|
@@ -176,6 +175,10 @@ function hasCloudModelInfo(cfg) {
|
|
|
176
175
|
return !!(cfg?.modelInfo && typeof cfg.modelInfo === 'object' && !Array.isArray(cfg.modelInfo));
|
|
177
176
|
}
|
|
178
177
|
|
|
178
|
+
function getAgentPatToken(cfg) {
|
|
179
|
+
return typeof cfg?.patToken === 'string' ? cfg.patToken.trim() : '';
|
|
180
|
+
}
|
|
181
|
+
|
|
179
182
|
function inferDeployTypeFromAgentConfig(cfg) {
|
|
180
183
|
if (cfg?.deployType === 'cloud') return { deployType: 'cloud', reason: 'config.deployType=cloud' };
|
|
181
184
|
if (isCloudRuntimeEnv()) return { deployType: 'cloud', reason: 'env CLOUD_ENV=1' };
|
|
@@ -216,15 +219,20 @@ function resolveAgent(agentId, soft) {
|
|
|
216
219
|
]);
|
|
217
220
|
}
|
|
218
221
|
const inferred = inferDeployTypeFromAgentConfig(cfg);
|
|
219
|
-
return { framework, workspace: cfg.workspace || '', deployType: inferred.deployType, deployReason: inferred.reason, agentId, root };
|
|
222
|
+
return { framework, workspace: cfg.workspace || '', deployType: inferred.deployType, deployReason: inferred.reason, agentId, root, patToken: getAgentPatToken(cfg) };
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
function validateArgs(args) {
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|
|
228
236
|
|
|
229
237
|
// --agent-id:优先读 coze-bridge 的 ~/.coze/agents/<id>/config.json 拿 framework/workspace。
|
|
230
238
|
// 云端判定优先看 deployType / CLOUD_ENV;兼容老 config 时再看 cloud-only 落盘字段。
|
|
@@ -239,12 +247,14 @@ function validateArgs(args) {
|
|
|
239
247
|
agentId: resolved.agentId,
|
|
240
248
|
workspace: resolved.workspace,
|
|
241
249
|
agentRoot: resolved.root,
|
|
250
|
+
patToken: resolved.patToken,
|
|
242
251
|
deployType: resolved.deployType,
|
|
243
252
|
deployReason: explicitCloud ? '--cloud' : resolved.deployReason,
|
|
244
253
|
'codex-home': args['codex-home'],
|
|
245
254
|
pairCode: args['pair-code'],
|
|
246
255
|
cloud,
|
|
247
256
|
force: !!args['force'],
|
|
257
|
+
verify: !!args['verify'],
|
|
248
258
|
};
|
|
249
259
|
}
|
|
250
260
|
// 显式 --cloud 或 CLOUD_ENV=1 且 config.json 缺失:回退到显式 --agent
|
|
@@ -268,6 +278,15 @@ function validateArgs(args) {
|
|
|
268
278
|
pairCode: args['pair-code'],
|
|
269
279
|
cloud: true,
|
|
270
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,
|
|
271
290
|
};
|
|
272
291
|
}
|
|
273
292
|
|
|
@@ -276,18 +295,15 @@ function validateArgs(args) {
|
|
|
276
295
|
'ERROR: --agent 或 --agent-id 至少提供一个',
|
|
277
296
|
'',
|
|
278
297
|
'Usage:',
|
|
279
|
-
' --agent
|
|
280
|
-
' --agent-
|
|
298
|
+
' --agent-id=<id> (本地/云端按 ~/.coze/agents/<id>/config.json 自动路由)',
|
|
299
|
+
' --cloud --agent=claude-code|codex|openclaw (云端兼容调用,token 来自环境变量)',
|
|
281
300
|
'',
|
|
282
301
|
'Flags:',
|
|
283
302
|
' --force 强制重装(OpenClaw 跳过幂等检查,无条件重写插件 + 重装依赖 + 重启 gateway)',
|
|
284
303
|
'',
|
|
285
304
|
'Other commands:',
|
|
286
|
-
' --status Show authorization status',
|
|
287
|
-
' --login Login (Device Code flow)',
|
|
288
|
-
' --refresh Force refresh access token',
|
|
289
|
-
' --logout Clear cached credentials',
|
|
290
305
|
' --verify Send a test trace to verify the reporting pipeline',
|
|
306
|
+
' 需要 --agent-id 的 patToken,或环境变量 COZELOOP_API_TOKEN/COZE_API_TOKEN',
|
|
291
307
|
' 可带 --pair-code=<值> 写入 trace metadata(缺省自动生成),供查询方回查',
|
|
292
308
|
]);
|
|
293
309
|
}
|
|
@@ -301,7 +317,17 @@ function validateArgs(args) {
|
|
|
301
317
|
' --agent=openclaw',
|
|
302
318
|
]);
|
|
303
319
|
}
|
|
304
|
-
|
|
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'] };
|
|
305
331
|
}
|
|
306
332
|
|
|
307
333
|
// ─── 4. Agent detection ──────────────────────────────────────────────────────
|
|
@@ -527,7 +553,7 @@ function shellEnvLine(key, value) {
|
|
|
527
553
|
// writeClaudeCodeHook 配置 Claude Code 的 hook。
|
|
528
554
|
// configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
|
|
529
555
|
// settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
|
|
530
|
-
function writeClaudeCodeHook(
|
|
556
|
+
function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud, tokenSource) {
|
|
531
557
|
const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
|
|
532
558
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
533
559
|
const baseDir = configBaseDir || process.cwd();
|
|
@@ -539,15 +565,13 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
539
565
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
540
566
|
const localSettingsPath = path.join(baseDir, '.claude', 'settings.local.json');
|
|
541
567
|
|
|
542
|
-
// 1. Write Python hook
|
|
568
|
+
// 1. Write Python hook script — 脚本放全局 ~/.claude/hooks,可共享
|
|
543
569
|
ensureDir(hooksDir);
|
|
544
570
|
writeHookScript(hookScript, readScript('claude-code/cozeloop_hook.py'));
|
|
545
|
-
const refreshScript = path.join(hooksDir, 'cozeloop_refresh.py');
|
|
546
|
-
writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
|
|
547
571
|
|
|
548
|
-
// 2. Merge settings.json — Stop (trace 收尾) + PostToolUse (trace 增量)
|
|
572
|
+
// 2. Merge settings.json — Stop (trace 收尾) + PostToolUse (trace 增量)。用绝对路径。
|
|
573
|
+
// 同时清掉旧版本写入的 cozeloop_refresh.py hook,避免继续走 OAuth refresh 兜底。
|
|
549
574
|
const hookCmd = `${pythonCmd} ${hookScript}`;
|
|
550
|
-
const refreshCmd = `${pythonCmd} ${refreshScript}`;
|
|
551
575
|
|
|
552
576
|
ensureDir(claudeDir);
|
|
553
577
|
let settings;
|
|
@@ -570,12 +594,11 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
570
594
|
);
|
|
571
595
|
existing.hooks.PostToolUse.push({ matcher: '', hooks: [{ type: 'command', command: hookCmd }] });
|
|
572
596
|
|
|
573
|
-
//
|
|
597
|
+
// Remove legacy token-refresh hook entries.
|
|
574
598
|
if (!existing.hooks.UserPromptSubmit) existing.hooks.UserPromptSubmit = [];
|
|
575
599
|
existing.hooks.UserPromptSubmit = existing.hooks.UserPromptSubmit.filter(
|
|
576
600
|
entry => !entry.hooks?.some(h => h.command?.includes('cozeloop_refresh.py'))
|
|
577
601
|
);
|
|
578
|
-
existing.hooks.UserPromptSubmit.push({ matcher: '', hooks: [{ type: 'command', command: refreshCmd }] });
|
|
579
602
|
|
|
580
603
|
return existing;
|
|
581
604
|
});
|
|
@@ -590,7 +613,7 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
590
613
|
}
|
|
591
614
|
ok(`Hook registered in ${settingsPath}`);
|
|
592
615
|
|
|
593
|
-
// 3. Write
|
|
616
|
+
// 3. Write hook environment into <baseDir>/.claude/settings.local.json
|
|
594
617
|
ensureDir(path.join(baseDir, '.claude'));
|
|
595
618
|
ensureDir(path.dirname(logFile));
|
|
596
619
|
let localSettings;
|
|
@@ -610,9 +633,16 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
610
633
|
existing.env.COZE_API_TOKEN = cozeToken;
|
|
611
634
|
delete existing.env.COZELOOP_API_TOKEN;
|
|
612
635
|
}
|
|
636
|
+
delete existing.env.COZELOOP_TOKEN_SOURCE;
|
|
637
|
+
} else if (tokenSource === TOKEN_SOURCE_AGENT_PAT && token) {
|
|
638
|
+
existing.env.COZELOOP_API_TOKEN = token;
|
|
639
|
+
existing.env.COZELOOP_TOKEN_SOURCE = TOKEN_SOURCE_AGENT_PAT;
|
|
640
|
+
delete existing.env.COZE_API_TOKEN;
|
|
641
|
+
delete existing.env.COZELAB_ONBOARD_CLOUD;
|
|
613
642
|
} else {
|
|
614
643
|
delete existing.env.COZELOOP_API_TOKEN;
|
|
615
644
|
delete existing.env.COZE_API_TOKEN;
|
|
645
|
+
delete existing.env.COZELOOP_TOKEN_SOURCE;
|
|
616
646
|
delete existing.env.COZELAB_ONBOARD_CLOUD;
|
|
617
647
|
}
|
|
618
648
|
const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
|
|
@@ -635,9 +665,9 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
635
665
|
try {
|
|
636
666
|
atomicWriteFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2));
|
|
637
667
|
} catch (e) {
|
|
638
|
-
errorBox([`ERROR: Cannot write
|
|
668
|
+
errorBox([`ERROR: Cannot write hook environment to ${localSettingsPath}`, '', e.message]);
|
|
639
669
|
}
|
|
640
|
-
ok(`
|
|
670
|
+
ok(`Hook environment written to ${localSettingsPath}`);
|
|
641
671
|
|
|
642
672
|
|
|
643
673
|
return { hookScript, settingsPath, localSettingsPath, logFile };
|
|
@@ -645,9 +675,9 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
|
|
|
645
675
|
|
|
646
676
|
// writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
|
|
647
677
|
// 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
|
|
648
|
-
//
|
|
678
|
+
// 本地 --agent-id 从 config.patToken 写入 token,并用 COZELOOP_TOKEN_SOURCE 标记来源。
|
|
649
679
|
// cloud=true 时写 COZELAB_ONBOARD_CLOUD,并带入 sandbox 注入的 trace token。
|
|
650
|
-
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
680
|
+
function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSource) {
|
|
651
681
|
const home = codexHome || path.join(os.homedir(), '.codex');
|
|
652
682
|
const hooksDir = path.join(home, 'hooks');
|
|
653
683
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
@@ -655,12 +685,9 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
|
655
685
|
const logFile = path.join(hooksDir, 'cozeloop.log');
|
|
656
686
|
const hooksJson = path.join(home, 'hooks.json');
|
|
657
687
|
|
|
658
|
-
// 1. Write Python hook
|
|
659
|
-
// Token is read from ~/.cozeloop/credentials.json at runtime
|
|
688
|
+
// 1. Write Python hook script
|
|
660
689
|
ensureDir(hooksDir);
|
|
661
690
|
writeHookScript(hookScript, readScript('codex/cozeloop_hook.py'));
|
|
662
|
-
const refreshScript = path.join(hooksDir, 'cozeloop_refresh.py');
|
|
663
|
-
writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
|
|
664
691
|
|
|
665
692
|
// 2. Write env file with chmod 600
|
|
666
693
|
const envLines = [
|
|
@@ -675,6 +702,9 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
|
675
702
|
} else if (cozeToken) {
|
|
676
703
|
envLines.push(shellEnvLine('COZE_API_TOKEN', cozeToken));
|
|
677
704
|
}
|
|
705
|
+
} else if (tokenSource === TOKEN_SOURCE_AGENT_PAT && token) {
|
|
706
|
+
envLines.push(shellEnvLine('COZELOOP_API_TOKEN', token));
|
|
707
|
+
envLines.push(shellEnvLine('COZELOOP_TOKEN_SOURCE', TOKEN_SOURCE_AGENT_PAT));
|
|
678
708
|
}
|
|
679
709
|
envLines.push(shellEnvLine('CODEX_HOME', home));
|
|
680
710
|
envLines.push(shellEnvLine('COZELOOP_HOOK_LOG', logFile));
|
|
@@ -690,14 +720,14 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
|
690
720
|
try {
|
|
691
721
|
fs.writeFileSync(envFile, envContent, { mode: 0o600 });
|
|
692
722
|
} catch (e) {
|
|
693
|
-
errorBox([`ERROR: Cannot write
|
|
723
|
+
errorBox([`ERROR: Cannot write hook environment to ${envFile}`, '', e.message]);
|
|
694
724
|
}
|
|
695
|
-
ok(`
|
|
725
|
+
ok(`Hook environment written to ${envFile} (chmod 600)`);
|
|
696
726
|
|
|
697
|
-
// 3. Merge hooks.json — Stop (trace 收尾) + PostToolUse (trace 增量)
|
|
727
|
+
// 3. Merge hooks.json — Stop (trace 收尾) + PostToolUse (trace 增量)
|
|
698
728
|
// 命令用绝对路径(CODEX_HOME 不一定是 ~/.codex)。
|
|
729
|
+
// 同时清掉旧版本写入的 cozeloop_refresh.py SessionStart hook。
|
|
699
730
|
const hookCmd = `set -a && . ${envFile} && set +a && ${pythonCmd} ${hookScript}`;
|
|
700
|
-
const refreshCmd = `${pythonCmd} ${refreshScript}`;
|
|
701
731
|
|
|
702
732
|
const hooks = mergeJson(hooksJson, (existing) => {
|
|
703
733
|
if (!existing.hooks) existing.hooks = {};
|
|
@@ -717,12 +747,11 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
|
|
|
717
747
|
);
|
|
718
748
|
existing.hooks.PostToolUse.push({ matcher: null, hooks: [{ type: 'command', command: hookCmd, timeout: 60 }] });
|
|
719
749
|
|
|
720
|
-
//
|
|
750
|
+
// Remove legacy token-refresh hook entries.
|
|
721
751
|
if (!existing.hooks.SessionStart) existing.hooks.SessionStart = [];
|
|
722
752
|
existing.hooks.SessionStart = existing.hooks.SessionStart.filter(
|
|
723
753
|
entry => !entry.hooks?.some(h => h.command?.includes('cozeloop_refresh.py'))
|
|
724
754
|
);
|
|
725
|
-
existing.hooks.SessionStart.push({ matcher: null, hooks: [{ type: 'command', command: refreshCmd, timeout: 15 }] });
|
|
726
755
|
|
|
727
756
|
return existing;
|
|
728
757
|
});
|
|
@@ -879,7 +908,7 @@ function buildOpenClawSelfcheckTags(pcfg, workspaceId, pair) {
|
|
|
879
908
|
return tags;
|
|
880
909
|
}
|
|
881
910
|
|
|
882
|
-
function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile) {
|
|
911
|
+
function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile, tokenSource) {
|
|
883
912
|
if (!existing.plugins) existing.plugins = {};
|
|
884
913
|
if (!existing.plugins.allow) existing.plugins.allow = [];
|
|
885
914
|
if (!existing.plugins.entries) existing.plugins.entries = {};
|
|
@@ -906,7 +935,12 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud,
|
|
|
906
935
|
if (logFile) {
|
|
907
936
|
pcfg.logFile = logFile;
|
|
908
937
|
}
|
|
909
|
-
|
|
938
|
+
if (tokenSource === TOKEN_SOURCE_AGENT_PAT) {
|
|
939
|
+
pcfg.tokenSource = TOKEN_SOURCE_AGENT_PAT;
|
|
940
|
+
} else {
|
|
941
|
+
delete pcfg.tokenSource;
|
|
942
|
+
}
|
|
943
|
+
pcfg.disableLocalCredentials = !!cloud || tokenSource === TOKEN_SOURCE_AGENT_PAT;
|
|
910
944
|
// 插件代码版本:参与幂等比对,升级插件(bump scripts/openclaw/package.json version)后
|
|
911
945
|
// 强制触发重写+重装+重启,避免云端 pluginDir 滞留旧插件 dist。
|
|
912
946
|
if (OPENCLAW_PLUGIN_VERSION) {
|
|
@@ -924,7 +958,7 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud,
|
|
|
924
958
|
return existing;
|
|
925
959
|
}
|
|
926
960
|
|
|
927
|
-
function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile) {
|
|
961
|
+
function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile, tokenSource) {
|
|
928
962
|
if (!fs.existsSync(pluginDir)) return false;
|
|
929
963
|
let existing;
|
|
930
964
|
try {
|
|
@@ -939,11 +973,12 @@ function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, ag
|
|
|
939
973
|
agentId,
|
|
940
974
|
cloud,
|
|
941
975
|
logFile,
|
|
976
|
+
tokenSource,
|
|
942
977
|
);
|
|
943
978
|
return JSON.stringify(existing) === JSON.stringify(desired);
|
|
944
979
|
}
|
|
945
980
|
|
|
946
|
-
function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
981
|
+
function writeOpenClawHook(token, workspaceId, agentId, cloud, force, tokenSource) {
|
|
947
982
|
const home = resolveHomeDir(cloud);
|
|
948
983
|
const configPath = path.join(home, '.openclaw', 'openclaw.json');
|
|
949
984
|
const pluginDir = path.join(home, '.cozeloop', 'openclaw-plugin');
|
|
@@ -958,7 +993,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
|
958
993
|
]);
|
|
959
994
|
}
|
|
960
995
|
|
|
961
|
-
if (!force && isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile)) {
|
|
996
|
+
if (!force && isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile, tokenSource)) {
|
|
962
997
|
ok(`OpenClaw plugin already configured in ${configPath}`);
|
|
963
998
|
info('OpenClaw gateway restart skipped (configuration unchanged).');
|
|
964
999
|
return { configPath, pluginDir, logFile, unchanged: true, gatewayRestarted: false, gatewayRestartSkipped: true };
|
|
@@ -1017,7 +1052,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
|
1017
1052
|
|
|
1018
1053
|
// 4. Update openclaw.json with token and workspace
|
|
1019
1054
|
const config = mergeJson(configPath, (existing) => {
|
|
1020
|
-
return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile);
|
|
1055
|
+
return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile, tokenSource);
|
|
1021
1056
|
});
|
|
1022
1057
|
|
|
1023
1058
|
try {
|
|
@@ -1040,7 +1075,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
|
|
|
1040
1075
|
}
|
|
1041
1076
|
}
|
|
1042
1077
|
|
|
1043
|
-
// ─── 8.
|
|
1078
|
+
// ─── 8. Trace verification HTTP helpers ─────────────────────────────────────
|
|
1044
1079
|
const https = require('https');
|
|
1045
1080
|
const crypto = require('crypto');
|
|
1046
1081
|
|
|
@@ -1354,18 +1389,15 @@ async function verifyTraceReport(token, workspaceId, pairCode, tracesUrl) {
|
|
|
1354
1389
|
}
|
|
1355
1390
|
|
|
1356
1391
|
// ── OpenClaw 专属上报链路校验 ──────────────────────────────────────────────
|
|
1357
|
-
// 为什么单独一条:claude-code/codex 的 verify
|
|
1392
|
+
// 为什么单独一条:claude-code/codex 的 verify 用主流程解析到的 token
|
|
1358
1393
|
// token 直发,而 openclaw 运行时上报用的是【写死在 openclaw.json 插件 config.authorization
|
|
1359
1394
|
// 里的静态 token】。两者是不同 token —— 插件那个失效(401/4100)时通用 verify 照样 ok,
|
|
1360
1395
|
// 这就是“verify=ok 但实际查不到 trace”假象的根因。本函数改为读插件实际配置的 token 打
|
|
1361
1396
|
// ingest,真实反映运行时会不会 401。
|
|
1362
1397
|
//
|
|
1363
1398
|
// cloud/local 兼容:插件配置位置都在 resolveHomeDir(cloud)/.openclaw/openclaw.json,
|
|
1364
|
-
// endpoint 都走 getOtelEndpointBase(cloud)
|
|
1365
|
-
//
|
|
1366
|
-
// 所以额外检测【实际加载的插件是否含刷新逻辑 getRefreshedToken】,无则告警。
|
|
1367
|
-
// - cloud:disableLocalCredentials=true,插件只用写死的 token、不刷新,token 失效需重注入,
|
|
1368
|
-
// 刷新能力检测对 cloud 无意义(跳过)。
|
|
1399
|
+
// endpoint 都走 getOtelEndpointBase(cloud),逻辑统一。OpenClaw 插件只使用 onboard 写入
|
|
1400
|
+
// 的 authorization,不再读取或刷新本地 credentials。
|
|
1369
1401
|
async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
1370
1402
|
const home = resolveHomeDir(cloud);
|
|
1371
1403
|
const configPath = path.join(home, '.openclaw', 'openclaw.json');
|
|
@@ -1457,60 +1489,14 @@ async function verifyOpenClawTraceLink(cloud, pairCode) {
|
|
|
1457
1489
|
if (snippet) console.log(snippet);
|
|
1458
1490
|
// 4100/401 = 该 token 已失效。指出根因与修复方式。
|
|
1459
1491
|
if (res.status === 401 || /\b4100\b/.test(res.body || '')) {
|
|
1460
|
-
info('插件配置的 token 已失效。运行时上报会 401
|
|
1461
|
-
info('修复:重跑 `
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
// 2) 仅 local:检测实际加载的插件是否具备 token 自动刷新能力。
|
|
1466
|
-
// cloud 主动 disableLocalCredentials,不刷新,检测无意义。
|
|
1467
|
-
if (!cloud) {
|
|
1468
|
-
const refreshOk = openClawPluginHasRefresh(home);
|
|
1469
|
-
if (refreshOk === true) {
|
|
1470
|
-
ok('openclaw 插件具备 token 自动刷新能力 (getRefreshedToken)。');
|
|
1471
|
-
} else if (refreshOk === false) {
|
|
1472
|
-
warn('本机加载的 openclaw 插件【无 token 刷新逻辑】,token 过期后会反复 401 崩 gateway。');
|
|
1473
|
-
info('修复:重跑 `node index.js --agent=openclaw --force` 安装带刷新逻辑的新插件。');
|
|
1492
|
+
info('插件配置的 token 已失效。运行时上报会 401,span 会丢失。');
|
|
1493
|
+
info('修复:重跑 `npx coze_lab --agent-id=<id> --force` 写入 agent config 中最新 patToken。');
|
|
1474
1494
|
}
|
|
1475
|
-
// refreshOk === null:定位不到插件文件,不下结论(不误报)。
|
|
1476
1495
|
}
|
|
1477
1496
|
|
|
1478
1497
|
return { success, status: res.status, body: res.body || '' };
|
|
1479
1498
|
}
|
|
1480
1499
|
|
|
1481
|
-
// 检测本机【实际加载的】openclaw trace 插件是否含 token 刷新逻辑(getRefreshedToken)。
|
|
1482
|
-
// 返回 true=有 / false=无 / null=定位不到插件文件(不下结论)。
|
|
1483
|
-
// 探测顺序:openclaw plugins list 给出的真实路径 > onboard 安装位置 ~/.cozeloop/openclaw-plugin
|
|
1484
|
-
// > 历史手改位置 ~/.openclaw/workspace/cozeloop-trace-fix。
|
|
1485
|
-
function openClawPluginHasRefresh(home) {
|
|
1486
|
-
const candidates = [];
|
|
1487
|
-
// openclaw plugins list 拿实际加载路径(最准——能发现 cozeloop-trace-fix 这类残留旧插件)
|
|
1488
|
-
try {
|
|
1489
|
-
const { execSync } = require('child_process');
|
|
1490
|
-
const out = execSync('openclaw plugins list', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
1491
|
-
for (const line of out.split(/\r?\n/)) {
|
|
1492
|
-
if (!/cozeloop|trace/i.test(line)) continue;
|
|
1493
|
-
const m = line.match(/(\/[^\s'"]+)/); // 抓行内绝对路径
|
|
1494
|
-
if (m) candidates.push(m[1]);
|
|
1495
|
-
}
|
|
1496
|
-
} catch { /* CLI 不可用则回退已知路径 */ }
|
|
1497
|
-
candidates.push(path.join(home, '.cozeloop', 'openclaw-plugin'));
|
|
1498
|
-
candidates.push(path.join(home, '.openclaw', 'workspace', 'cozeloop-trace-fix'));
|
|
1499
|
-
|
|
1500
|
-
let foundAny = false;
|
|
1501
|
-
for (const base of candidates) {
|
|
1502
|
-
for (const rel of ['dist/cozeloop-exporter.js', 'dist/index.js', 'cozeloop-exporter.js', 'index.js']) {
|
|
1503
|
-
const f = path.isAbsolute(rel) ? rel : path.join(base, rel);
|
|
1504
|
-
try {
|
|
1505
|
-
if (!fs.existsSync(f)) continue;
|
|
1506
|
-
foundAny = true;
|
|
1507
|
-
if (fs.readFileSync(f, 'utf8').includes('getRefreshedToken')) return true;
|
|
1508
|
-
} catch { /* ignore */ }
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
return foundAny ? false : null;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1500
|
function httpsGet(url, headers) {
|
|
1515
1501
|
return new Promise((resolve, reject) => {
|
|
1516
1502
|
const h = { ...(headers || {}) };
|
|
@@ -1524,254 +1510,10 @@ function httpsGet(url, headers) {
|
|
|
1524
1510
|
});
|
|
1525
1511
|
}
|
|
1526
1512
|
|
|
1527
|
-
// ── Credentials store ──────────────────────────────────────────────────────
|
|
1528
|
-
function loadCredentials() {
|
|
1529
|
-
try {
|
|
1530
|
-
return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'));
|
|
1531
|
-
} catch {
|
|
1532
|
-
return null;
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function saveCredentials(creds) {
|
|
1537
|
-
// 0o700:凭证目录仅 owner 可读/进入,其他用户无法枚举 ~/.cozeloop 内容。
|
|
1538
|
-
fs.mkdirSync(path.dirname(CREDS_PATH), { recursive: true, mode: 0o700 });
|
|
1539
|
-
atomicWriteFileSync(CREDS_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
function deleteCredentials() {
|
|
1543
|
-
try { fs.unlinkSync(CREDS_PATH); } catch { /* already gone */ }
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
function isExpired(creds) {
|
|
1547
|
-
if (!creds || !creds.expires_at) return true;
|
|
1548
|
-
return Date.now() >= creds.expires_at - REFRESH_THRESHOLD_MS;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
// ── Refresh token ──────────────────────────────────────────────────────────
|
|
1552
|
-
async function refreshToken(creds) {
|
|
1553
|
-
info('Access token expiring soon, refreshing...');
|
|
1554
|
-
let res;
|
|
1555
|
-
try {
|
|
1556
|
-
res = await httpsPost(`${COZE_API}/api/permission/oauth2/token`, {
|
|
1557
|
-
grant_type: 'refresh_token',
|
|
1558
|
-
client_id: CLIENT_ID,
|
|
1559
|
-
refresh_token: creds.refresh_token,
|
|
1560
|
-
});
|
|
1561
|
-
} catch (e) {
|
|
1562
|
-
errorBox(['ERROR: Could not refresh token', '', e.message]);
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
let data;
|
|
1566
|
-
try { data = JSON.parse(res.body); } catch {
|
|
1567
|
-
errorBox(['ERROR: Unexpected response while refreshing token']);
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
if (data.error || res.status !== 200) {
|
|
1571
|
-
warn(`Token refresh failed (${data.error || res.status}), re-authorizing...`);
|
|
1572
|
-
return null; // caller will re-run device code flow
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
const updated = {
|
|
1576
|
-
access_token: data.access_token,
|
|
1577
|
-
refresh_token: data.refresh_token ?? creds.refresh_token,
|
|
1578
|
-
expires_at: (data.expires_in ?? 0) * 1000, // expires_at stored in milliseconds (Python 端按毫秒读)
|
|
1579
|
-
workspace_id: creds.workspace_id ?? WORKSPACE_ID, // preserve workspace_id
|
|
1580
|
-
};
|
|
1581
|
-
saveCredentials(updated);
|
|
1582
|
-
ok('Token refreshed successfully.');
|
|
1583
|
-
return updated;
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
// ── Device Code flow ───────────────────────────────────────────────────────
|
|
1587
|
-
async function deviceCodeAuth() {
|
|
1588
|
-
// Step 1: Get device code
|
|
1589
|
-
let res;
|
|
1590
|
-
try {
|
|
1591
|
-
res = await httpsPost(`${COZE_API}/api/permission/oauth2/device/code`, {
|
|
1592
|
-
client_id: CLIENT_ID,
|
|
1593
|
-
});
|
|
1594
|
-
} catch (e) {
|
|
1595
|
-
errorBox(['ERROR: Could not reach Coze API', '', e.message]);
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
let data;
|
|
1599
|
-
try { data = JSON.parse(res.body); } catch {
|
|
1600
|
-
errorBox(['ERROR: Unexpected response from Coze API']);
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
if (res.status !== 200 || data.error) {
|
|
1604
|
-
errorBox([
|
|
1605
|
-
'ERROR: Failed to start device authorization',
|
|
1606
|
-
'',
|
|
1607
|
-
data.error_description || data.error || `HTTP ${res.status}`,
|
|
1608
|
-
]);
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
const { device_code, user_code, verification_uri, expires_in, interval = 5 } = data;
|
|
1612
|
-
const activation_url = `${verification_uri}?user_code=${encodeURIComponent(user_code)}`;
|
|
1613
|
-
|
|
1614
|
-
console.log('');
|
|
1615
|
-
box([
|
|
1616
|
-
' 请在浏览器中打开以下链接完成授权:',
|
|
1617
|
-
'',
|
|
1618
|
-
` ${activation_url}`,
|
|
1619
|
-
'',
|
|
1620
|
-
` 验证码将在 ${expires_in} 秒后过期`,
|
|
1621
|
-
], C.cyan);
|
|
1622
|
-
console.log('');
|
|
1623
|
-
|
|
1624
|
-
// Try to open browser automatically
|
|
1625
|
-
try {
|
|
1626
|
-
const { execSync } = require('child_process');
|
|
1627
|
-
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1628
|
-
execSync(`${opener} "${activation_url}"`, { stdio: 'ignore' });
|
|
1629
|
-
info('已自动打开浏览器,请在浏览器中完成授权...');
|
|
1630
|
-
} catch {
|
|
1631
|
-
info('请手动在浏览器中打开上方链接完成授权...');
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
// Step 2: Poll for token
|
|
1635
|
-
const deadline = Date.now() + expires_in * 1000;
|
|
1636
|
-
let pollInterval = interval * 1000;
|
|
1637
|
-
|
|
1638
|
-
process.stdout.write(`${C.cyan}[i]${C.reset} 等待授权中`);
|
|
1639
|
-
|
|
1640
|
-
while (Date.now() < deadline) {
|
|
1641
|
-
await new Promise(r => setTimeout(r, pollInterval));
|
|
1642
|
-
process.stdout.write('.');
|
|
1643
|
-
|
|
1644
|
-
let pollRes;
|
|
1645
|
-
try {
|
|
1646
|
-
pollRes = await httpsPost(`${COZE_API}/api/permission/oauth2/token`, {
|
|
1647
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
1648
|
-
client_id: CLIENT_ID,
|
|
1649
|
-
device_code,
|
|
1650
|
-
});
|
|
1651
|
-
} catch { continue; }
|
|
1652
|
-
|
|
1653
|
-
let pollData;
|
|
1654
|
-
try { pollData = JSON.parse(pollRes.body); } catch { continue; }
|
|
1655
|
-
|
|
1656
|
-
if (pollData.access_token) {
|
|
1657
|
-
process.stdout.write('\n');
|
|
1658
|
-
const creds = {
|
|
1659
|
-
access_token: pollData.access_token,
|
|
1660
|
-
refresh_token: pollData.refresh_token,
|
|
1661
|
-
expires_at: (pollData.expires_in ?? 0) * 1000, // expires_at stored in milliseconds (Python 端按毫秒读)
|
|
1662
|
-
workspace_id: WORKSPACE_ID,
|
|
1663
|
-
};
|
|
1664
|
-
saveCredentials(creds);
|
|
1665
|
-
console.log('');
|
|
1666
|
-
ok('授权成功!Token 已保存到 ~/.cozeloop/credentials.json');
|
|
1667
|
-
return creds;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
if (pollData.error === 'slow_down') {
|
|
1671
|
-
pollInterval += 5000;
|
|
1672
|
-
} else if (pollData.error === 'access_denied') {
|
|
1673
|
-
process.stdout.write('\n');
|
|
1674
|
-
errorBox(['ERROR: 用户拒绝了授权请求']);
|
|
1675
|
-
} else if (pollData.error === 'expired_token') {
|
|
1676
|
-
process.stdout.write('\n');
|
|
1677
|
-
errorBox(['ERROR: 验证码已过期,请重新运行命令']);
|
|
1678
|
-
}
|
|
1679
|
-
// authorization_pending → keep polling
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
process.stdout.write('\n');
|
|
1683
|
-
errorBox(['ERROR: 授权超时,请重新运行命令']);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// ── Get valid token (load → refresh → re-auth) ────────────────────────────
|
|
1687
|
-
async function getValidToken() {
|
|
1688
|
-
let creds = loadCredentials();
|
|
1689
|
-
|
|
1690
|
-
if (creds && !isExpired(creds)) {
|
|
1691
|
-
ok('已找到有效的本地授权,跳过授权步骤。');
|
|
1692
|
-
return creds.access_token;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
if (creds && creds.refresh_token) {
|
|
1696
|
-
const refreshed = await refreshToken(creds);
|
|
1697
|
-
if (refreshed) return refreshed.access_token;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// Need fresh authorization
|
|
1701
|
-
info('未找到本地授权,启动 Device Code 授权流程...');
|
|
1702
|
-
const fresh = await deviceCodeAuth();
|
|
1703
|
-
return fresh.access_token;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
// ── Logout ────────────────────────────────────────────────────────────────
|
|
1707
|
-
function logout() {
|
|
1708
|
-
const creds = loadCredentials();
|
|
1709
|
-
if (!creds) {
|
|
1710
|
-
info('本地没有保存的授权信息,无需退出。');
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
deleteCredentials();
|
|
1714
|
-
successBox(['已成功退出登录', '', '本地 Token 已清除 (~/.cozeloop/credentials.json)']);
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
// ── Auth status ───────────────────────────────────────────────────────────
|
|
1718
|
-
function authStatus() {
|
|
1719
|
-
const creds = loadCredentials();
|
|
1720
|
-
if (!creds) {
|
|
1721
|
-
box([
|
|
1722
|
-
'Auth Status: NOT LOGGED IN',
|
|
1723
|
-
'',
|
|
1724
|
-
'~/.cozeloop/credentials.json not found.',
|
|
1725
|
-
'Run with --agent=<type> to authorize.',
|
|
1726
|
-
], C.yellow);
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
const expiresAt = creds.expires_at ? new Date(creds.expires_at) : null;
|
|
1731
|
-
const now = Date.now();
|
|
1732
|
-
const remainMs = creds.expires_at ? creds.expires_at - now : null;
|
|
1733
|
-
const remainMin = remainMs != null ? Math.floor(remainMs / 60000) : null;
|
|
1734
|
-
|
|
1735
|
-
function formatRemaining(ms) {
|
|
1736
|
-
if (ms == null) return 'unknown';
|
|
1737
|
-
const min = Math.floor(ms / 60000);
|
|
1738
|
-
if (min < 60) return `${min} 分钟`;
|
|
1739
|
-
const hr = Math.floor(min / 60);
|
|
1740
|
-
if (hr < 24) return `${hr} 小时`;
|
|
1741
|
-
const day = Math.floor(hr / 24);
|
|
1742
|
-
if (day < 365) return `${day} 天`;
|
|
1743
|
-
return `${Math.floor(day / 365)} 年`;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
let statusLabel, statusColor;
|
|
1747
|
-
if (!expiresAt) {
|
|
1748
|
-
statusLabel = 'UNKNOWN (no expiry info)';
|
|
1749
|
-
statusColor = C.yellow;
|
|
1750
|
-
} else if (remainMs < 0) {
|
|
1751
|
-
statusLabel = 'EXPIRED';
|
|
1752
|
-
statusColor = C.red;
|
|
1753
|
-
} else if (remainMs < REFRESH_THRESHOLD_MS) {
|
|
1754
|
-
statusLabel = `EXPIRING SOON (剩余 ${formatRemaining(remainMs)})`;
|
|
1755
|
-
statusColor = C.yellow;
|
|
1756
|
-
} else {
|
|
1757
|
-
statusLabel = `VALID (剩余 ${formatRemaining(remainMs)})`;
|
|
1758
|
-
statusColor = C.green;
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
const lines = [
|
|
1762
|
-
`Auth Status: ${statusLabel}`,
|
|
1763
|
-
'',
|
|
1764
|
-
`Token: ${creds.access_token ? creds.access_token.slice(0, 20) + '...' : 'n/a'}`,
|
|
1765
|
-
`Expires at: ${expiresAt ? expiresAt.toLocaleString() : 'unknown'}`,
|
|
1766
|
-
`Refresh: ${creds.refresh_token ? 'available' : 'not available'}`,
|
|
1767
|
-
];
|
|
1768
|
-
box(lines, statusColor);
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
1513
|
// ─── 9. Main ─────────────────────────────────────────────────────────────────
|
|
1772
1514
|
const NEXT_STEP = {
|
|
1773
1515
|
'claude-code': 'Hook 已写入。Claude Code 会自动热重载 hooks,当前会话即刻生效,无需 /new 或重启。',
|
|
1774
|
-
'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex
|
|
1516
|
+
'codex': 'Hook 已写入。Codex 在会话启动时加载 hook,当前会话不会即时生效;请重开 Codex 会话。',
|
|
1775
1517
|
};
|
|
1776
1518
|
|
|
1777
1519
|
function openClawNextStep(written) {
|
|
@@ -1791,64 +1533,31 @@ async function main() {
|
|
|
1791
1533
|
|
|
1792
1534
|
const args = validateArgs(parseArgs());
|
|
1793
1535
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
if (
|
|
1810
|
-
|
|
1811
|
-
|
|
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',
|
|
1812
1557
|
'',
|
|
1813
|
-
'
|
|
1558
|
+
'请确认 ~/.coze/agents/<id>/config.json 包含 patToken 后重试。',
|
|
1814
1559
|
]);
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
deleteCredentials();
|
|
1818
|
-
await deviceCodeAuth();
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
if (args.refresh) {
|
|
1823
|
-
const creds = loadCredentials();
|
|
1824
|
-
if (!creds) {
|
|
1825
|
-
box([
|
|
1826
|
-
'ERROR: 未找到本地授权信息',
|
|
1827
|
-
'',
|
|
1828
|
-
'请先运行 --agent=<type> 完成授权。',
|
|
1829
|
-
], C.red);
|
|
1830
|
-
process.exit(1);
|
|
1831
1560
|
}
|
|
1832
|
-
if (!creds.refresh_token) {
|
|
1833
|
-
box([
|
|
1834
|
-
'ERROR: 当前凭证没有 refresh_token',
|
|
1835
|
-
'',
|
|
1836
|
-
'请运行 --logout 后重新 --agent=<type> 授权。',
|
|
1837
|
-
], C.red);
|
|
1838
|
-
process.exit(1);
|
|
1839
|
-
}
|
|
1840
|
-
const refreshed = await refreshToken(creds);
|
|
1841
|
-
if (!refreshed) {
|
|
1842
|
-
warn('Refresh token 已失效,启动 Device Code 重新授权...');
|
|
1843
|
-
console.log('');
|
|
1844
|
-
await deviceCodeAuth();
|
|
1845
|
-
}
|
|
1846
|
-
return;
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
if (args.verify) {
|
|
1850
|
-
info('验证 trace 上报链路...');
|
|
1851
|
-
const token = await getValidToken(); // 无凭证会自动走登录/刷新
|
|
1852
1561
|
console.log('');
|
|
1853
1562
|
const result = await verifyTraceReport(token, WORKSPACE_ID, args.pairCode, getOtelTracesUrl(false));
|
|
1854
1563
|
// 若本机装了 openclaw 插件,额外校验插件【实际配置的静态 token】——通用 verify 用刚刷新的
|
|
@@ -1861,7 +1570,7 @@ async function main() {
|
|
|
1861
1570
|
if (cfg?.plugins?.entries?.['openclaw-cozeloop-trace']?.config?.authorization) {
|
|
1862
1571
|
console.log('');
|
|
1863
1572
|
info('检测到 openclaw cozeloop-trace 插件,校验其实际 token...');
|
|
1864
|
-
const ocRes = await verifyOpenClawTraceLink(
|
|
1573
|
+
const ocRes = await verifyOpenClawTraceLink(args.cloud, args.pairCode);
|
|
1865
1574
|
ocOk = ocRes.success;
|
|
1866
1575
|
}
|
|
1867
1576
|
} catch { /* 读不了就跳过 openclaw 校验 */ }
|
|
@@ -1883,10 +1592,10 @@ async function main() {
|
|
|
1883
1592
|
console.log('');
|
|
1884
1593
|
}
|
|
1885
1594
|
|
|
1886
|
-
// Step 1:
|
|
1887
|
-
// 云端模式:token 取自 sandbox
|
|
1595
|
+
// Step 1: Resolve trace token.
|
|
1596
|
+
// 云端模式:token 取自 sandbox 注入的环境变量。
|
|
1888
1597
|
// 优先使用 COZELOOP_API_TOKEN;兼容使用 COZE_API_TOKEN,并以真实 selfcheck 为准。
|
|
1889
|
-
//
|
|
1598
|
+
// 本地模式:必须通过 --agent-id 读取 config.patToken。
|
|
1890
1599
|
// 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
|
|
1891
1600
|
let token;
|
|
1892
1601
|
let tokenSource = '';
|
|
@@ -1909,9 +1618,21 @@ async function main() {
|
|
|
1909
1618
|
info('将兼容使用 COZE_API_TOKEN 作为 trace token,并通过 selfcheck 验证实际可用性。');
|
|
1910
1619
|
}
|
|
1911
1620
|
} else {
|
|
1912
|
-
info('Step 1/5:
|
|
1913
|
-
|
|
1914
|
-
|
|
1621
|
+
info('Step 1/5: 从 agent config 读取 patToken...');
|
|
1622
|
+
if (args.agentId && args.patToken) {
|
|
1623
|
+
token = args.patToken;
|
|
1624
|
+
tokenSource = TOKEN_SOURCE_AGENT_PAT;
|
|
1625
|
+
ok(`已从 ~/.coze/agents/${args.agentId}/config.json 读取 patToken。`);
|
|
1626
|
+
} else {
|
|
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
|
+
]);
|
|
1635
|
+
}
|
|
1915
1636
|
}
|
|
1916
1637
|
console.log('');
|
|
1917
1638
|
|
|
@@ -1945,7 +1666,7 @@ async function main() {
|
|
|
1945
1666
|
'否则 hook 配置会落到全局/进程目录,无法做到 per-agent 隔离。',
|
|
1946
1667
|
]);
|
|
1947
1668
|
}
|
|
1948
|
-
written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud);
|
|
1669
|
+
written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud, tokenSource);
|
|
1949
1670
|
} else if (agent === 'codex') {
|
|
1950
1671
|
const codexHome = resolveCodexHome(args);
|
|
1951
1672
|
if (args.cloud && args.agentId && codexHome && !fs.existsSync(codexHome)) {
|
|
@@ -1953,10 +1674,10 @@ async function main() {
|
|
|
1953
1674
|
info(`已创建云端 Codex 配置目录: ${codexHome}`);
|
|
1954
1675
|
}
|
|
1955
1676
|
if (codexHome) info(`Codex 配置目录: ${codexHome}`);
|
|
1956
|
-
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
|
|
1677
|
+
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud, tokenSource);
|
|
1957
1678
|
} else {
|
|
1958
1679
|
// openclaw:云端用 traceAgentIds allowlist 做 per-agent 放行。
|
|
1959
|
-
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force) || {};
|
|
1680
|
+
written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force, tokenSource) || {};
|
|
1960
1681
|
}
|
|
1961
1682
|
// 走到这里说明 detectAgent / 环境检查 / 写 hook 配置全部成功 → 注入成功。
|
|
1962
1683
|
cloudResult.inject = 'ok';
|
|
@@ -1995,7 +1716,7 @@ async function main() {
|
|
|
1995
1716
|
|
|
1996
1717
|
// Step 5: Verify trace reporting end-to-end
|
|
1997
1718
|
info('Step 5/5: 验证 trace 上报链路...');
|
|
1998
|
-
// openclaw 走专属校验:claude-code/codex 的 verify
|
|
1719
|
+
// openclaw 走专属校验:claude-code/codex 的 verify 用主流程解析到的
|
|
1999
1720
|
// 有效 token 直发,测不到 openclaw 插件【写死在 openclaw.json 里的静态 token】是否失效
|
|
2000
1721
|
// (插件不读这个临时 token)。openclaw 必须用插件实际配置的 authorization 打 ingest,
|
|
2001
1722
|
// 才能真实反映运行时上报会不会 401。cloud/local 配置位置一致,统一走这条。
|
|
@@ -2056,7 +1777,7 @@ if (require.main === module) {
|
|
|
2056
1777
|
|
|
2057
1778
|
module.exports = {
|
|
2058
1779
|
resolveHomeDir,
|
|
1780
|
+
getAgentPatToken,
|
|
2059
1781
|
mergeJson,
|
|
2060
1782
|
atomicWriteFileSync,
|
|
2061
|
-
isExpired,
|
|
2062
1783
|
};
|