coze_lab 0.1.4 → 0.1.5
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 +144 -44
- package/package.json +1 -1
- package/scripts/claude-code/cozeloop_hook.py +76 -7
- package/scripts/codex/cozeloop_hook.py +79 -8
- package/scripts/openclaw/dist/cozeloop-exporter.js +3 -1
- package/scripts/openclaw/dist/index.js +48 -5
- package/scripts/openclaw/dist/index.js.bak-20260601222111 +1315 -0
package/index.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// ─── 0. Constants ─────────────────────────────────────────────────────────────
|
|
5
5
|
const CLIENT_ID = '46371084383473718052118955183420.app.coze';
|
|
6
6
|
const WORKSPACE_ID = '7644910356078837760';
|
|
7
|
-
//
|
|
8
|
-
const
|
|
7
|
+
// PPE 泳道:所有 cozeloop 上报 / OAuth 请求都带这两个 header(SDK 读 x_tt_env/x_use_ppe 环境变量自动注入)。
|
|
8
|
+
const PPE_TT_ENV = 'ppe_cozelab';
|
|
9
|
+
const PPE_USE_PPE = '1';
|
|
9
10
|
const COZE_API = 'https://api.coze.cn';
|
|
10
11
|
const CREDS_PATH = require('path').join(require('os').homedir(), '.cozeloop', 'credentials.json');
|
|
11
12
|
// Refresh when less than 10 minutes remain
|
|
@@ -3457,7 +3458,7 @@ async function _refreshToken(refreshTok) {
|
|
|
3457
3458
|
const body = JSON.stringify({ grant_type: "refresh_token", client_id: _CLIENT_ID, refresh_token: refreshTok });
|
|
3458
3459
|
const req = https.request(\`\${_COZE_API}/api/permission/oauth2/token\`, {
|
|
3459
3460
|
method: "POST",
|
|
3460
|
-
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
3461
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "x-tt-env": "ppe_cozelab", "x-use-ppe": "1" },
|
|
3461
3462
|
}, (res) => {
|
|
3462
3463
|
let buf = "";
|
|
3463
3464
|
res.on("data", c => buf += c);
|
|
@@ -3591,6 +3592,8 @@ export class CozeloopExporter {
|
|
|
3591
3592
|
headers: {
|
|
3592
3593
|
"Authorization": authorization,
|
|
3593
3594
|
"cozeloop-workspace-id": workspaceId,
|
|
3595
|
+
"x-tt-env": "ppe_cozelab",
|
|
3596
|
+
"x-use-ppe": "1",
|
|
3594
3597
|
},
|
|
3595
3598
|
});
|
|
3596
3599
|
this.provider = new BasicTracerProvider({ resource });
|
|
@@ -4119,20 +4122,61 @@ function parseArgs() {
|
|
|
4119
4122
|
|
|
4120
4123
|
const VALID_AGENTS = ['claude-code', 'codex', 'openclaw'];
|
|
4121
4124
|
|
|
4125
|
+
// resolveAgent 读 ~/.coze/agents/<agentId>/config.json,返回 { framework, workspace, agentId, root }。
|
|
4126
|
+
// 目录或 config 不存在、framework 非法时直接报错退出。
|
|
4127
|
+
function resolveAgent(agentId) {
|
|
4128
|
+
const root = path.join(os.homedir(), '.coze', 'agents', agentId);
|
|
4129
|
+
const configPath = path.join(root, 'config.json');
|
|
4130
|
+
if (!fs.existsSync(configPath)) {
|
|
4131
|
+
errorBox([
|
|
4132
|
+
`ERROR: agent "${agentId}" 不存在`,
|
|
4133
|
+
'',
|
|
4134
|
+
`未找到 ${configPath}`,
|
|
4135
|
+
'请确认 agentId 正确,或先用 coze-bridge 创建该 agent。',
|
|
4136
|
+
]);
|
|
4137
|
+
}
|
|
4138
|
+
let cfg;
|
|
4139
|
+
try {
|
|
4140
|
+
cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
4141
|
+
} catch (e) {
|
|
4142
|
+
errorBox([`ERROR: 解析 ${configPath} 失败`, '', e.message]);
|
|
4143
|
+
}
|
|
4144
|
+
const framework = cfg.framework;
|
|
4145
|
+
if (!VALID_AGENTS.includes(framework)) {
|
|
4146
|
+
errorBox([
|
|
4147
|
+
`ERROR: agent "${agentId}" 的 framework="${framework}" 不受支持`,
|
|
4148
|
+
'',
|
|
4149
|
+
`支持的类型: ${VALID_AGENTS.join(', ')}`,
|
|
4150
|
+
]);
|
|
4151
|
+
}
|
|
4152
|
+
return { framework, workspace: cfg.workspace || '', agentId, root };
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4122
4155
|
function validateArgs(args) {
|
|
4123
4156
|
if (args['logout']) return { logout: true };
|
|
4124
4157
|
if (args['login']) return { login: true };
|
|
4125
4158
|
if (args['status']) return { status: true };
|
|
4126
4159
|
if (args['refresh']) return { refresh: true };
|
|
4127
4160
|
|
|
4161
|
+
// 优先 --agent-id:读 ~/.coze/agents/<id>/config.json 的 framework 自动路由。
|
|
4162
|
+
if (args['agent-id']) {
|
|
4163
|
+
const resolved = resolveAgent(args['agent-id']);
|
|
4164
|
+
return {
|
|
4165
|
+
agent: resolved.framework,
|
|
4166
|
+
agentId: resolved.agentId,
|
|
4167
|
+
workspace: resolved.workspace,
|
|
4168
|
+
agentRoot: resolved.root,
|
|
4169
|
+
'codex-home': args['codex-home'],
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4128
4173
|
if (!args['agent']) {
|
|
4129
4174
|
errorBox([
|
|
4130
|
-
'ERROR: --agent
|
|
4175
|
+
'ERROR: --agent 或 --agent-id 至少提供一个',
|
|
4131
4176
|
'',
|
|
4132
4177
|
'Usage:',
|
|
4133
|
-
' --agent=claude-code',
|
|
4134
|
-
' --agent
|
|
4135
|
-
' --agent=openclaw',
|
|
4178
|
+
' --agent=claude-code | codex | openclaw (全局配置)',
|
|
4179
|
+
' --agent-id=<id> (按 ~/.coze/agents/<id> 的 framework 自动路由)',
|
|
4136
4180
|
'',
|
|
4137
4181
|
'Other commands:',
|
|
4138
4182
|
' --status Show authorization status',
|
|
@@ -4151,7 +4195,7 @@ function validateArgs(args) {
|
|
|
4151
4195
|
' --agent=openclaw',
|
|
4152
4196
|
]);
|
|
4153
4197
|
}
|
|
4154
|
-
return { agent: args['agent'] };
|
|
4198
|
+
return { agent: args['agent'], 'codex-home': args['codex-home'] };
|
|
4155
4199
|
}
|
|
4156
4200
|
|
|
4157
4201
|
// ─── 4. Agent detection ──────────────────────────────────────────────────────
|
|
@@ -4339,22 +4383,31 @@ function mergeJson(filepath, mergeFn) {
|
|
|
4339
4383
|
return mergeFn(existing);
|
|
4340
4384
|
}
|
|
4341
4385
|
|
|
4342
|
-
|
|
4386
|
+
// writeClaudeCodeHook 配置 Claude Code 的 hook。
|
|
4387
|
+
// configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
|
|
4388
|
+
// settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
|
|
4389
|
+
function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir) {
|
|
4343
4390
|
const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
|
|
4344
4391
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
4345
|
-
const
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4392
|
+
const baseDir = configBaseDir || process.cwd();
|
|
4393
|
+
// per-agent 时 settings.json 写进 agent 的 .claude;全局时仍写 ~/.claude/settings.json。
|
|
4394
|
+
const claudeDir = configBaseDir
|
|
4395
|
+
? path.join(baseDir, '.claude')
|
|
4396
|
+
: path.join(os.homedir(), '.claude');
|
|
4397
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
4398
|
+
const localSettingsPath = path.join(baseDir, '.claude', 'settings.local.json');
|
|
4399
|
+
|
|
4400
|
+
// 1. Write Python hook scripts (trace + refresh) — 脚本放全局 ~/.claude/hooks,可共享
|
|
4349
4401
|
ensureDir(hooksDir);
|
|
4350
4402
|
writeHookScript(hookScript, readScript('claude-code/cozeloop_hook.py'));
|
|
4351
4403
|
const refreshScript = path.join(hooksDir, 'cozeloop_refresh.py');
|
|
4352
4404
|
writeHookScript(refreshScript, readScript('shared/cozeloop_refresh.py'));
|
|
4353
4405
|
|
|
4354
|
-
// 2. Merge
|
|
4355
|
-
const hookCmd = `${pythonCmd}
|
|
4356
|
-
const refreshCmd = `${pythonCmd}
|
|
4406
|
+
// 2. Merge settings.json — Stop (trace) + UserPromptSubmit (refresh)。用绝对路径。
|
|
4407
|
+
const hookCmd = `${pythonCmd} ${hookScript}`;
|
|
4408
|
+
const refreshCmd = `${pythonCmd} ${refreshScript}`;
|
|
4357
4409
|
|
|
4410
|
+
ensureDir(claudeDir);
|
|
4358
4411
|
const settings = mergeJson(settingsPath, (existing) => {
|
|
4359
4412
|
if (!existing.hooks) existing.hooks = {};
|
|
4360
4413
|
|
|
@@ -4379,14 +4432,17 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd) {
|
|
|
4379
4432
|
} catch (e) {
|
|
4380
4433
|
errorBox([`ERROR: Cannot write ${settingsPath}`, '', e.message]);
|
|
4381
4434
|
}
|
|
4382
|
-
ok(`
|
|
4435
|
+
ok(`Hook registered in ${settingsPath}`);
|
|
4383
4436
|
|
|
4384
|
-
// 3. Write
|
|
4385
|
-
ensureDir(path.join(
|
|
4437
|
+
// 3. Write credentials into <baseDir>/.claude/settings.local.json
|
|
4438
|
+
ensureDir(path.join(baseDir, '.claude'));
|
|
4386
4439
|
const localSettings = mergeJson(localSettingsPath, (existing) => {
|
|
4387
4440
|
if (!existing.env) existing.env = {};
|
|
4388
4441
|
existing.env.COZELOOP_WORKSPACE_ID = workspaceId;
|
|
4389
4442
|
existing.env.COZELOOP_API_TOKEN = patToken;
|
|
4443
|
+
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4444
|
+
existing.env.x_tt_env = PPE_TT_ENV;
|
|
4445
|
+
existing.env.x_use_ppe = PPE_USE_PPE;
|
|
4390
4446
|
return existing;
|
|
4391
4447
|
});
|
|
4392
4448
|
try {
|
|
@@ -4400,11 +4456,14 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd) {
|
|
|
4400
4456
|
return { hookScript, settingsPath, localSettingsPath };
|
|
4401
4457
|
}
|
|
4402
4458
|
|
|
4403
|
-
|
|
4404
|
-
|
|
4459
|
+
// writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
|
|
4460
|
+
// 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
|
|
4461
|
+
function writeCodexHook(token, workspaceId, pythonCmd, codexHome) {
|
|
4462
|
+
const home = codexHome || path.join(os.homedir(), '.codex');
|
|
4463
|
+
const hooksDir = path.join(home, 'hooks');
|
|
4405
4464
|
const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
|
|
4406
4465
|
const envFile = path.join(hooksDir, 'cozeloop.env');
|
|
4407
|
-
const hooksJson = path.join(
|
|
4466
|
+
const hooksJson = path.join(home, 'hooks.json');
|
|
4408
4467
|
|
|
4409
4468
|
// 1. Write Python hook scripts (trace + refresh)
|
|
4410
4469
|
// Token is read from ~/.cozeloop/credentials.json at runtime
|
|
@@ -4418,6 +4477,9 @@ function writeCodexHook(token, workspaceId, pythonCmd) {
|
|
|
4418
4477
|
`COZELOOP_WORKSPACE_ID=${workspaceId}`,
|
|
4419
4478
|
`COZELOOP_API_TOKEN=${token}`,
|
|
4420
4479
|
'TRACE_TO_COZELOOP=true',
|
|
4480
|
+
// PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
|
|
4481
|
+
`x_tt_env=${PPE_TT_ENV}`,
|
|
4482
|
+
`x_use_ppe=${PPE_USE_PPE}`,
|
|
4421
4483
|
].join('\n') + '\n';
|
|
4422
4484
|
try {
|
|
4423
4485
|
fs.writeFileSync(envFile, envContent, { mode: 0o600 });
|
|
@@ -4427,8 +4489,9 @@ function writeCodexHook(token, workspaceId, pythonCmd) {
|
|
|
4427
4489
|
ok(`Credentials written to ${envFile} (chmod 600)`);
|
|
4428
4490
|
|
|
4429
4491
|
// 3. Merge hooks.json — Stop (trace) + SessionStart (refresh)
|
|
4430
|
-
|
|
4431
|
-
const
|
|
4492
|
+
// 命令用绝对路径(CODEX_HOME 不一定是 ~/.codex)。
|
|
4493
|
+
const hookCmd = `set -a && . ${envFile} && set +a && ${pythonCmd} ${hookScript}`;
|
|
4494
|
+
const refreshCmd = `${pythonCmd} ${refreshScript}`;
|
|
4432
4495
|
|
|
4433
4496
|
const hooks = mergeJson(hooksJson, (existing) => {
|
|
4434
4497
|
if (!existing.hooks) existing.hooks = {};
|
|
@@ -4553,6 +4616,17 @@ function writeOpenClawHook(token, workspaceId) {
|
|
|
4553
4616
|
errorBox([`ERROR: Cannot write ${configPath}`, '', e.message]);
|
|
4554
4617
|
}
|
|
4555
4618
|
ok(`OpenClaw plugin configured in ${configPath}`);
|
|
4619
|
+
|
|
4620
|
+
// 自动重启 gateway,让新插件配置立即生效(不依赖 plugins install 的隐式重启)。
|
|
4621
|
+
try {
|
|
4622
|
+
const { execSync } = require('child_process');
|
|
4623
|
+
info('Restarting OpenClaw gateway to apply hook changes...');
|
|
4624
|
+
execSync('openclaw gateway restart', { stdio: 'pipe' });
|
|
4625
|
+
ok('OpenClaw gateway restarted');
|
|
4626
|
+
} catch (e) {
|
|
4627
|
+
warn(`gateway restart 失败,请手动执行: openclaw gateway restart(${e.message})`);
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4556
4630
|
return { configPath, pluginDir };
|
|
4557
4631
|
}
|
|
4558
4632
|
|
|
@@ -4565,7 +4639,8 @@ function httpsPost(url, body) {
|
|
|
4565
4639
|
const u = new URL(url);
|
|
4566
4640
|
const req = https.request(
|
|
4567
4641
|
{ hostname: u.hostname, path: u.pathname + u.search, method: 'POST',
|
|
4568
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data)
|
|
4642
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data),
|
|
4643
|
+
'x-tt-env': PPE_TT_ENV, 'x-use-ppe': PPE_USE_PPE } },
|
|
4569
4644
|
(res) => {
|
|
4570
4645
|
let buf = '';
|
|
4571
4646
|
res.on('data', c => buf += c);
|
|
@@ -4581,7 +4656,9 @@ function httpsPost(url, body) {
|
|
|
4581
4656
|
|
|
4582
4657
|
function httpsGet(url, headers) {
|
|
4583
4658
|
return new Promise((resolve, reject) => {
|
|
4584
|
-
|
|
4659
|
+
// 合并 PPE 泳道 header
|
|
4660
|
+
const h = { ...(headers || {}), 'x-tt-env': PPE_TT_ENV, 'x-use-ppe': PPE_USE_PPE };
|
|
4661
|
+
const req = https.get(url, { headers: h }, (res) => {
|
|
4585
4662
|
let buf = '';
|
|
4586
4663
|
res.on('data', c => buf += c);
|
|
4587
4664
|
res.on('end', () => resolve({ status: res.statusCode, body: buf }));
|
|
@@ -4847,9 +4924,9 @@ async function verifyWorkspace(token) {
|
|
|
4847
4924
|
|
|
4848
4925
|
// ─── 9. Main ─────────────────────────────────────────────────────────────────
|
|
4849
4926
|
const NEXT_STEP = {
|
|
4850
|
-
'claude-code': '
|
|
4851
|
-
'codex': '
|
|
4852
|
-
'openclaw': '
|
|
4927
|
+
'claude-code': 'Hook 已写入。请在当前 Claude Code 会话运行 /new(或重开会话)使 hook 生效——外部进程无法替你 /new。',
|
|
4928
|
+
'codex': 'Hook 已写入。请重开 Codex 会话使 hook 生效——外部进程无法替你重启当前会话。',
|
|
4929
|
+
'openclaw': 'OpenClaw gateway 已自动重启,trace 即刻生效。',
|
|
4853
4930
|
};
|
|
4854
4931
|
|
|
4855
4932
|
async function main() {
|
|
@@ -4915,14 +4992,31 @@ async function main() {
|
|
|
4915
4992
|
}
|
|
4916
4993
|
|
|
4917
4994
|
const { agent } = args;
|
|
4995
|
+
// per-agent 路由时 agent 的 workspace(claude-code 用它做配置根目录)。
|
|
4996
|
+
const agentWorkspace = args.workspace || '';
|
|
4997
|
+
if (args.agentId) {
|
|
4998
|
+
info(`目标 agent: ${args.agentId} (framework=${agent}, workspace=${agentWorkspace || 'N/A'})`);
|
|
4999
|
+
console.log('');
|
|
5000
|
+
}
|
|
4918
5001
|
|
|
4919
|
-
// Step 1: Authorize
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
let token = HARDCODED_TOKEN;
|
|
5002
|
+
// Step 1: Authorize (load cached → refresh → device code)
|
|
5003
|
+
info('Step 1/5: 检查授权状态...');
|
|
5004
|
+
let token = await getValidToken();
|
|
4923
5005
|
console.log('');
|
|
4924
5006
|
|
|
4925
|
-
|
|
5007
|
+
// Step 2: Verify token works
|
|
5008
|
+
info('Step 2/5: 验证工作空间权限...');
|
|
5009
|
+
let verified = await verifyWorkspace(token);
|
|
5010
|
+
if (verified === null) {
|
|
5011
|
+
warn('Token 已失效,正在重新授权...');
|
|
5012
|
+
deleteCredentials();
|
|
5013
|
+
console.log('');
|
|
5014
|
+
token = await deviceCodeAuth().then(c => c.access_token);
|
|
5015
|
+
verified = await verifyWorkspace(token);
|
|
5016
|
+
if (verified === null) {
|
|
5017
|
+
errorBox(['ERROR: 重新授权后 Token 仍无效,请联系管理员。']);
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
4926
5020
|
console.log('');
|
|
4927
5021
|
|
|
4928
5022
|
// Step 3: Detect agent binary
|
|
@@ -4942,27 +5036,33 @@ async function main() {
|
|
|
4942
5036
|
|
|
4943
5037
|
// Step 5: Write hook configuration
|
|
4944
5038
|
info('Step 5/5: Writing hook configuration...');
|
|
5039
|
+
let written = {};
|
|
4945
5040
|
if (agent === 'claude-code') {
|
|
4946
|
-
|
|
5041
|
+
// per-agent 路由时把配置写进 agent 的 workspace(仅该 agent 生效)。
|
|
5042
|
+
written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined);
|
|
4947
5043
|
} else if (agent === 'codex') {
|
|
4948
|
-
|
|
5044
|
+
// CODEX_HOME 来源优先级:--codex-home= > 环境变量 CODEX_HOME > ~/.codex(缺省)。
|
|
5045
|
+
const codexHome = args['codex-home'] || process.env.CODEX_HOME || undefined;
|
|
5046
|
+
if (codexHome) info(`Codex 配置目录: ${codexHome}`);
|
|
5047
|
+
written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome);
|
|
4949
5048
|
} else {
|
|
4950
|
-
writeOpenClawHook(token, WORKSPACE_ID);
|
|
5049
|
+
written = writeOpenClawHook(token, WORKSPACE_ID) || {};
|
|
4951
5050
|
}
|
|
4952
5051
|
console.log('');
|
|
4953
5052
|
|
|
4954
|
-
// Success summary
|
|
5053
|
+
// Success summary(用实际写入路径,per-agent / 自定义 CODEX_HOME 时也准确)
|
|
4955
5054
|
const summaryLines = [
|
|
4956
5055
|
`Agent: ${agent} v${version}`,
|
|
4957
5056
|
];
|
|
5057
|
+
if (args.agentId) summaryLines.push(`Agent ID: ${args.agentId}`);
|
|
4958
5058
|
if (agent === 'claude-code') {
|
|
4959
|
-
summaryLines.push(`Hook script: ~/.claude/hooks/cozeloop_hook.py`);
|
|
4960
|
-
summaryLines.push(`Config: ~/.claude/settings.json`);
|
|
4961
|
-
summaryLines.push(`Credentials: .claude/settings.local.json`);
|
|
5059
|
+
summaryLines.push(`Hook script: ${written.hookScript || '~/.claude/hooks/cozeloop_hook.py'}`);
|
|
5060
|
+
summaryLines.push(`Config: ${written.settingsPath || '~/.claude/settings.json'}`);
|
|
5061
|
+
summaryLines.push(`Credentials: ${written.localSettingsPath || '.claude/settings.local.json'}`);
|
|
4962
5062
|
} else if (agent === 'codex') {
|
|
4963
|
-
summaryLines.push(`Hook script: ~/.codex/hooks/cozeloop_hook.py`);
|
|
4964
|
-
summaryLines.push(`Config: ~/.codex/hooks.json`);
|
|
4965
|
-
summaryLines.push(`Credentials: ~/.codex/hooks/cozeloop.env`);
|
|
5063
|
+
summaryLines.push(`Hook script: ${written.hookScript || '~/.codex/hooks/cozeloop_hook.py'}`);
|
|
5064
|
+
summaryLines.push(`Config: ${written.hooksJson || '~/.codex/hooks.json'}`);
|
|
5065
|
+
summaryLines.push(`Credentials: ${written.envFile || '~/.codex/hooks/cozeloop.env'}`);
|
|
4966
5066
|
} else {
|
|
4967
5067
|
summaryLines.push(`Config: ~/.openclaw/openclaw.json`);
|
|
4968
5068
|
}
|
package/package.json
CHANGED
|
@@ -45,7 +45,7 @@ DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
|
|
|
45
45
|
_COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
|
|
46
46
|
_COZE_API = "https://api.coze.cn"
|
|
47
47
|
_REFRESH_THRESHOLD = 10 * 60 # refresh when < 10 minutes remain
|
|
48
|
-
_DEFAULT_WORKSPACE_ID = "
|
|
48
|
+
_DEFAULT_WORKSPACE_ID = "7645949103524380682" # hardcoded spaceID fallback
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
# --- coze-context parsing -------------------------------------------------
|
|
@@ -431,6 +431,29 @@ def _group_subagent_steps(progress_msgs: List[Dict[str, Any]]) -> List[Dict[str,
|
|
|
431
431
|
return steps
|
|
432
432
|
|
|
433
433
|
|
|
434
|
+
def _parse_ts(msg):
|
|
435
|
+
"""从 transcript msg 的 timestamp 字段解析出 datetime(带时区)。失败返回 None。
|
|
436
|
+
|
|
437
|
+
Claude Code transcript 每条 user/assistant 消息都带 ISO8601 timestamp
|
|
438
|
+
(如 "2026-06-01T13:51:55.450Z")。建 span 时用它做 start_time,
|
|
439
|
+
避免回放时所有 span 挤在几毫秒内、duration 全是几毫秒。
|
|
440
|
+
"""
|
|
441
|
+
if not isinstance(msg, dict):
|
|
442
|
+
return None
|
|
443
|
+
ts = msg.get("timestamp")
|
|
444
|
+
if not ts or not isinstance(ts, str):
|
|
445
|
+
return None
|
|
446
|
+
try:
|
|
447
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
448
|
+
except Exception:
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _ts_ms(dt):
|
|
453
|
+
"""datetime → 毫秒时间戳(int);None → None。"""
|
|
454
|
+
return int(dt.timestamp() * 1000) if dt is not None else None
|
|
455
|
+
|
|
456
|
+
|
|
434
457
|
def group_messages_into_turns(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
435
458
|
"""Group messages into conversation turns (user -> assistant -> tool_results).
|
|
436
459
|
|
|
@@ -793,13 +816,31 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
793
816
|
client = cozeloop.new_client(**client_kwargs)
|
|
794
817
|
|
|
795
818
|
try:
|
|
796
|
-
|
|
819
|
+
# 整体时间范围:第一条 user 消息 → 最后一条 assistant 消息(真实 transcript 时间)
|
|
820
|
+
_all_ts = []
|
|
821
|
+
for _t in turns:
|
|
822
|
+
_u = _parse_ts(_t.get("user_message"))
|
|
823
|
+
if _u:
|
|
824
|
+
_all_ts.append(_u)
|
|
825
|
+
for _s in _t.get("steps", []):
|
|
826
|
+
_a = _parse_ts(_s.get("assistant_message"))
|
|
827
|
+
if _a:
|
|
828
|
+
_all_ts.append(_a)
|
|
829
|
+
root_start_dt = min(_all_ts) if _all_ts else None
|
|
830
|
+
root_end_dt = max(_all_ts) if _all_ts else None
|
|
831
|
+
|
|
832
|
+
with client.start_span(name="claude_code_request", span_type="main", start_time=root_start_dt) as root_span:
|
|
797
833
|
root_span.set_runtime(Runtime(library="claude-code"))
|
|
798
834
|
root_tags = {
|
|
799
835
|
"thread_id": session_id,
|
|
800
836
|
"total_turns": len(turns),
|
|
801
837
|
"source": "claude_code"
|
|
802
838
|
}
|
|
839
|
+
# 真实耗时写 tag(SDK finish 不支持显式结束时间,duration 用 tag 兜底)
|
|
840
|
+
if root_start_dt is not None and root_end_dt is not None:
|
|
841
|
+
root_tags["real_start_ms"] = _ts_ms(root_start_dt)
|
|
842
|
+
root_tags["real_end_ms"] = _ts_ms(root_end_dt)
|
|
843
|
+
root_tags["latency_ms"] = _ts_ms(root_end_dt) - _ts_ms(root_start_dt)
|
|
803
844
|
root_baggage = {
|
|
804
845
|
"thread_id": session_id,
|
|
805
846
|
}
|
|
@@ -836,14 +877,29 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
836
877
|
steps = turn.get("steps", [])
|
|
837
878
|
total_steps = len(steps)
|
|
838
879
|
|
|
839
|
-
|
|
880
|
+
# turn 真实时间:start=user 消息时间,end=最后一个 step 的 assistant 时间
|
|
881
|
+
turn_start_dt = _parse_ts(turn.get("user_message"))
|
|
882
|
+
turn_end_dt = None
|
|
883
|
+
for _s in steps:
|
|
884
|
+
_a = _parse_ts(_s.get("assistant_message"))
|
|
885
|
+
if _a:
|
|
886
|
+
turn_end_dt = _a
|
|
887
|
+
if turn_start_dt is None:
|
|
888
|
+
turn_start_dt = turn_end_dt
|
|
889
|
+
|
|
890
|
+
with client.start_span(name=f"turn_{i}", span_type="main", start_time=turn_start_dt) as turn_span:
|
|
840
891
|
turn_span.set_runtime(Runtime(library="claude-code"))
|
|
841
|
-
|
|
892
|
+
_turn_tags = {
|
|
842
893
|
"thread_id": session_id,
|
|
843
894
|
"turn_index": i,
|
|
844
895
|
"total_steps": total_steps,
|
|
845
896
|
"source": "claude_code",
|
|
846
|
-
}
|
|
897
|
+
}
|
|
898
|
+
if turn_start_dt is not None and turn_end_dt is not None:
|
|
899
|
+
_turn_tags["real_start_ms"] = _ts_ms(turn_start_dt)
|
|
900
|
+
_turn_tags["real_end_ms"] = _ts_ms(turn_end_dt)
|
|
901
|
+
_turn_tags["latency_ms"] = _ts_ms(turn_end_dt) - _ts_ms(turn_start_dt)
|
|
902
|
+
turn_span.set_tags(_turn_tags)
|
|
847
903
|
|
|
848
904
|
# Extract user input for this turn
|
|
849
905
|
user_message = turn.get("user_message", {}).get("message", {})
|
|
@@ -861,10 +917,23 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
861
917
|
raw_content = assistant_message_obj.get("content", [])
|
|
862
918
|
model_name = assistant_message_obj.get("model", "claude-code")
|
|
863
919
|
|
|
920
|
+
# step 真实时间:start=本 step assistant 时间;end=下一 step assistant 时间(即本次模型调用+工具的耗时),无下一 step 则用 turn_end
|
|
921
|
+
step_start_dt = _parse_ts(assistant_msg)
|
|
922
|
+
if j + 1 < len(steps):
|
|
923
|
+
step_end_dt = _parse_ts(steps[j + 1].get("assistant_message")) or turn_end_dt
|
|
924
|
+
else:
|
|
925
|
+
step_end_dt = turn_end_dt
|
|
926
|
+
|
|
864
927
|
# --- Create model span for this step ---
|
|
865
|
-
with client.start_span(name=f"model_call_{j}", span_type="model") as model_span:
|
|
928
|
+
with client.start_span(name=f"model_call_{j}", span_type="model", start_time=step_start_dt) as model_span:
|
|
866
929
|
model_span.set_runtime(Runtime(library="claude-code"))
|
|
867
930
|
model_span.set_model_name(model_name)
|
|
931
|
+
if step_start_dt is not None and step_end_dt is not None:
|
|
932
|
+
model_span.set_tags({
|
|
933
|
+
"real_start_ms": _ts_ms(step_start_dt),
|
|
934
|
+
"real_end_ms": _ts_ms(step_end_dt),
|
|
935
|
+
"latency_ms": _ts_ms(step_end_dt) - _ts_ms(step_start_dt),
|
|
936
|
+
})
|
|
868
937
|
|
|
869
938
|
# Set input: accumulated context up to this point
|
|
870
939
|
model_span.set_input(ModelInput(
|
|
@@ -958,7 +1027,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
958
1027
|
span_type = "agent" if is_agent else "tool"
|
|
959
1028
|
span_name = f"agent_{tool_name}" if is_agent else f"tool_{tool_name}"
|
|
960
1029
|
|
|
961
|
-
with client.start_span(name=span_name, span_type=span_type) as tool_span:
|
|
1030
|
+
with client.start_span(name=span_name, span_type=span_type, start_time=step_start_dt) as tool_span:
|
|
962
1031
|
tool_span.set_runtime(Runtime(library="claude-code"))
|
|
963
1032
|
tags = {
|
|
964
1033
|
"tool_name": tool_name,
|
|
@@ -45,7 +45,7 @@ from typing import Optional, List, Dict, Any
|
|
|
45
45
|
_COZELOOP_CLIENT_ID = "46371084383473718052118955183420.app.coze"
|
|
46
46
|
_COZE_API = "https://api.coze.cn"
|
|
47
47
|
_REFRESH_THRESHOLD = 10 * 60
|
|
48
|
-
_DEFAULT_WORKSPACE_ID = "
|
|
48
|
+
_DEFAULT_WORKSPACE_ID = "7645949103524380682" # hardcoded spaceID fallback
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
# --- coze-context parsing -------------------------------------------------
|
|
@@ -415,6 +415,28 @@ def truncate_text(text: str, limit: int = 12000) -> str:
|
|
|
415
415
|
|
|
416
416
|
# --- Message Grouping ---
|
|
417
417
|
|
|
418
|
+
def _parse_ts(obj):
|
|
419
|
+
"""从 codex entry/payload 的 timestamp 解析 datetime(带时区)。失败返回 None。
|
|
420
|
+
|
|
421
|
+
Codex rollout JSONL 每条 entry 顶层带 ISO8601 timestamp。建 span 时用它做
|
|
422
|
+
start_time,避免回放时所有 span 挤在几毫秒内。
|
|
423
|
+
"""
|
|
424
|
+
if not isinstance(obj, dict):
|
|
425
|
+
return None
|
|
426
|
+
ts = obj.get("timestamp") or obj.get("_ts")
|
|
427
|
+
if not ts or not isinstance(ts, str):
|
|
428
|
+
return None
|
|
429
|
+
try:
|
|
430
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
431
|
+
except Exception:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _ts_ms(dt):
|
|
436
|
+
"""datetime → 毫秒时间戳(int);None → None。"""
|
|
437
|
+
return int(dt.timestamp() * 1000) if dt is not None else None
|
|
438
|
+
|
|
439
|
+
|
|
418
440
|
def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
419
441
|
"""Group raw JSONL entries into conversation turns.
|
|
420
442
|
|
|
@@ -437,6 +459,9 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
437
459
|
for entry in entries:
|
|
438
460
|
entry_type = entry.get("type")
|
|
439
461
|
payload = entry.get("payload", {})
|
|
462
|
+
# 把 entry 顶层 timestamp 注入 payload,供后续建 span 取真实时间
|
|
463
|
+
if isinstance(payload, dict) and entry.get("timestamp") and "_ts" not in payload:
|
|
464
|
+
payload["_ts"] = entry.get("timestamp")
|
|
440
465
|
|
|
441
466
|
# --- Turn lifecycle events ---
|
|
442
467
|
if entry_type == "event_msg":
|
|
@@ -645,13 +670,30 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
645
670
|
ctx: List[Dict[str, Any]] = list(history_context) if history_context else []
|
|
646
671
|
|
|
647
672
|
try:
|
|
648
|
-
|
|
673
|
+
# 整体时间范围:所有 user/assistant payload 的真实 timestamp 极值
|
|
674
|
+
_all_ts = []
|
|
675
|
+
for _t in turns:
|
|
676
|
+
_u = _parse_ts(_t.get("user_message"))
|
|
677
|
+
if _u:
|
|
678
|
+
_all_ts.append(_u)
|
|
679
|
+
for _a in _t.get("assistant_messages", []):
|
|
680
|
+
_ad = _parse_ts(_a)
|
|
681
|
+
if _ad:
|
|
682
|
+
_all_ts.append(_ad)
|
|
683
|
+
root_start_dt = min(_all_ts) if _all_ts else None
|
|
684
|
+
root_end_dt = max(_all_ts) if _all_ts else None
|
|
685
|
+
|
|
686
|
+
with client.start_span(name="codex_request", span_type="main", start_time=root_start_dt) as root_span:
|
|
649
687
|
root_span.set_runtime(Runtime(library="codex-cli"))
|
|
650
688
|
root_tags = {
|
|
651
689
|
"thread_id": session_id,
|
|
652
690
|
"total_turns": len(turns),
|
|
653
691
|
"source": "codex_cli",
|
|
654
692
|
}
|
|
693
|
+
if root_start_dt is not None and root_end_dt is not None:
|
|
694
|
+
root_tags["real_start_ms"] = _ts_ms(root_start_dt)
|
|
695
|
+
root_tags["real_end_ms"] = _ts_ms(root_end_dt)
|
|
696
|
+
root_tags["latency_ms"] = _ts_ms(root_end_dt) - _ts_ms(root_start_dt)
|
|
655
697
|
root_baggage = {
|
|
656
698
|
"thread_id": session_id,
|
|
657
699
|
}
|
|
@@ -689,20 +731,49 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
689
731
|
# Process each turn
|
|
690
732
|
for i, turn in enumerate(turns):
|
|
691
733
|
try:
|
|
692
|
-
|
|
734
|
+
# turn 真实时间:start=user payload 时间;end=最后一条 assistant payload 时间
|
|
735
|
+
turn_start_dt = _parse_ts(turn.get("user_message"))
|
|
736
|
+
turn_end_dt = None
|
|
737
|
+
for _a in turn.get("assistant_messages", []):
|
|
738
|
+
_ad = _parse_ts(_a)
|
|
739
|
+
if _ad:
|
|
740
|
+
turn_end_dt = _ad
|
|
741
|
+
if turn_start_dt is None:
|
|
742
|
+
turn_start_dt = turn_end_dt
|
|
743
|
+
|
|
744
|
+
with client.start_span(name=f"turn_{i}", span_type="main", start_time=turn_start_dt) as turn_span:
|
|
693
745
|
turn_span.set_runtime(Runtime(library="codex-cli"))
|
|
694
|
-
|
|
746
|
+
_turn_tags = {
|
|
695
747
|
"thread_id": session_id,
|
|
696
748
|
"turn_index": i,
|
|
697
749
|
"turn_id": turn.get("turn_id", ""),
|
|
698
750
|
"source": "codex_cli",
|
|
699
|
-
}
|
|
751
|
+
}
|
|
752
|
+
if turn_start_dt is not None and turn_end_dt is not None:
|
|
753
|
+
_turn_tags["real_start_ms"] = _ts_ms(turn_start_dt)
|
|
754
|
+
_turn_tags["real_end_ms"] = _ts_ms(turn_end_dt)
|
|
755
|
+
_turn_tags["latency_ms"] = _ts_ms(turn_end_dt) - _ts_ms(turn_start_dt)
|
|
756
|
+
turn_span.set_tags(_turn_tags)
|
|
700
757
|
|
|
701
758
|
# --- Model span for assistant response ---
|
|
702
759
|
if turn.get("assistant_messages"):
|
|
703
|
-
|
|
760
|
+
# model span start:第一条 assistant payload 时间,回退到 turn 起点
|
|
761
|
+
_model_start_dt = None
|
|
762
|
+
for _a in turn.get("assistant_messages", []):
|
|
763
|
+
_model_start_dt = _parse_ts(_a)
|
|
764
|
+
if _model_start_dt:
|
|
765
|
+
break
|
|
766
|
+
if _model_start_dt is None:
|
|
767
|
+
_model_start_dt = turn_start_dt
|
|
768
|
+
with client.start_span(name="assistant_response", span_type="model", start_time=_model_start_dt) as model_span:
|
|
704
769
|
model_span.set_runtime(Runtime(library="codex-cli"))
|
|
705
770
|
model_span.set_model_name(model_name)
|
|
771
|
+
if _model_start_dt is not None and turn_end_dt is not None:
|
|
772
|
+
model_span.set_tags({
|
|
773
|
+
"real_start_ms": _ts_ms(_model_start_dt),
|
|
774
|
+
"real_end_ms": _ts_ms(turn_end_dt),
|
|
775
|
+
"latency_ms": _ts_ms(turn_end_dt) - _ts_ms(_model_start_dt),
|
|
776
|
+
})
|
|
706
777
|
|
|
707
778
|
# Build input messages: history + current turn input
|
|
708
779
|
turn_input = turn.get("input_messages", [])
|
|
@@ -770,7 +841,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
770
841
|
# --- Tool call spans ---
|
|
771
842
|
for tool_call in turn.get("tool_calls", []):
|
|
772
843
|
tool_name = tool_call.get("name", "unknown")
|
|
773
|
-
with client.start_span(name=f"tool_{tool_name}", span_type="tool") as tool_span:
|
|
844
|
+
with client.start_span(name=f"tool_{tool_name}", span_type="tool", start_time=turn_start_dt) as tool_span:
|
|
774
845
|
tool_span.set_runtime(Runtime(library="codex-cli"))
|
|
775
846
|
tool_span.set_tags({
|
|
776
847
|
"tool_name": tool_name,
|
|
@@ -794,7 +865,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
794
865
|
agent_id = sc.get("agent_id") or "unknown"
|
|
795
866
|
nickname = sc.get("nickname") or agent_id
|
|
796
867
|
|
|
797
|
-
with client.start_span(name=f"subagent_{nickname}", span_type="agent") as subagent_span:
|
|
868
|
+
with client.start_span(name=f"subagent_{nickname}", span_type="agent", start_time=turn_start_dt) as subagent_span:
|
|
798
869
|
subagent_span.set_runtime(Runtime(library="codex-cli"))
|
|
799
870
|
subagent_span.set_tags({
|
|
800
871
|
"agent_id": agent_id,
|
|
@@ -36,7 +36,7 @@ async function _refreshToken(refreshTok) {
|
|
|
36
36
|
const body = JSON.stringify({ grant_type: "refresh_token", client_id: _CLIENT_ID, refresh_token: refreshTok });
|
|
37
37
|
const req = https.request(`${_COZE_API}/api/permission/oauth2/token`, {
|
|
38
38
|
method: "POST",
|
|
39
|
-
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
39
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "x-tt-env": "ppe_cozelab", "x-use-ppe": "1" },
|
|
40
40
|
}, (res) => {
|
|
41
41
|
let buf = "";
|
|
42
42
|
res.on("data", c => buf += c);
|
|
@@ -170,6 +170,8 @@ export class CozeloopExporter {
|
|
|
170
170
|
headers: {
|
|
171
171
|
"Authorization": authorization,
|
|
172
172
|
"cozeloop-workspace-id": workspaceId,
|
|
173
|
+
"x-tt-env": "ppe_cozelab",
|
|
174
|
+
"x-use-ppe": "1",
|
|
173
175
|
},
|
|
174
176
|
});
|
|
175
177
|
this.provider = new BasicTracerProvider({ resource });
|