oh-langfuse 0.1.60 → 0.1.62
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 +245 -245
- package/bin/cli.js +22 -22
- package/codex_langfuse_notify.py +422 -56
- package/package.json +6 -6
- package/scripts/auto-update-runtime.mjs +56 -54
- package/scripts/codex-langfuse-setup.mjs +127 -117
- package/scripts/langfuse-setup.mjs +71 -71
- package/scripts/opencode-langfuse-run.mjs +23 -23
- package/scripts/opencode-langfuse-setup.mjs +144 -69
- package/scripts/real-self-verify.mjs +114 -104
- package/scripts/update-langfuse-runtime.mjs +7 -6
- package/scripts/verify-update-runtime.mjs +162 -162
- package/scripts/verify-update-utils.mjs +60 -60
|
@@ -81,7 +81,7 @@ function extractZipWithPowerShell({ zipPath, destDir }) {
|
|
|
81
81
|
const cmd = cmdParts.join(" ");
|
|
82
82
|
|
|
83
83
|
try {
|
|
84
|
-
execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
84
|
+
execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", encoding: "utf8" });
|
|
85
85
|
} catch {
|
|
86
86
|
throw new Error("解压 hooks zip 失败:请确认 zip 文件存在,且 PowerShell 支持 Expand-Archive。");
|
|
87
87
|
}
|
|
@@ -97,7 +97,7 @@ function downloadAndExtractZipWithPowerShell({ zipUrl, destDir }) {
|
|
|
97
97
|
].join(" ");
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
|
-
execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
100
|
+
execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", encoding: "utf8" });
|
|
101
101
|
} catch (e) {
|
|
102
102
|
const msg = e?.message ? String(e.message) : "";
|
|
103
103
|
if (msg.includes("(401)") || msg.toLowerCase().includes("unauthorized")) {
|
|
@@ -176,7 +176,7 @@ function writeAutoUpdateHelper(target) {
|
|
|
176
176
|
"exit /b 0",
|
|
177
177
|
""
|
|
178
178
|
];
|
|
179
|
-
fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
|
|
179
|
+
fs.writeFileSync(helper, "\ufeff" + lines.join(os.EOL), "utf8");
|
|
180
180
|
return helper;
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -263,7 +263,7 @@ function prependWindowsUserPath(dir) {
|
|
|
263
263
|
" [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
|
|
264
264
|
"}"
|
|
265
265
|
].join(" ");
|
|
266
|
-
const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true });
|
|
266
|
+
const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true, encoding: "utf8" });
|
|
267
267
|
if (result.status !== 0) throw new Error("Failed to prepend Claude Langfuse shim to the user PATH.");
|
|
268
268
|
|
|
269
269
|
const currentParts = String(process.env.PATH || "").split(path.delimiter);
|
|
@@ -294,7 +294,7 @@ function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey
|
|
|
294
294
|
"exit /b %ERRORLEVEL%",
|
|
295
295
|
""
|
|
296
296
|
].filter(Boolean).join(os.EOL);
|
|
297
|
-
fs.writeFileSync(shim, content, "utf8");
|
|
297
|
+
fs.writeFileSync(shim, "\ufeff" + content, "utf8");
|
|
298
298
|
return { shim, shimDir };
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -326,7 +326,7 @@ function createAgentLauncher({ baseDir, target, executable }) {
|
|
|
326
326
|
`${executable} %*`,
|
|
327
327
|
""
|
|
328
328
|
].join(os.EOL);
|
|
329
|
-
fs.writeFileSync(launcher, content, "utf8");
|
|
329
|
+
fs.writeFileSync(launcher, "\ufeff" + content, "utf8");
|
|
330
330
|
return launcher;
|
|
331
331
|
}
|
|
332
332
|
|
|
@@ -343,54 +343,54 @@ function createAgentLauncher({ baseDir, target, executable }) {
|
|
|
343
343
|
return launcher;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
function pythonExecutableInVenv(venvDir) {
|
|
347
|
-
return process.platform === "win32"
|
|
348
|
-
? path.join(venvDir, "Scripts", "python.exe")
|
|
349
|
-
: path.join(venvDir, "bin", "python");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function pythonCanImport(pythonCmd, moduleName) {
|
|
353
|
-
try {
|
|
354
|
-
execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
|
|
355
|
-
return true;
|
|
356
|
-
} catch {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
|
|
362
|
-
const attempts = [
|
|
363
|
-
{ command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
|
|
364
|
-
{ command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
|
|
365
|
-
{ command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
|
|
366
|
-
];
|
|
367
|
-
|
|
368
|
-
const errors = [];
|
|
369
|
-
for (const attempt of attempts) {
|
|
370
|
-
try {
|
|
371
|
-
console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
|
|
372
|
-
execFileSync(attempt.command, attempt.args, { stdio: "inherit" });
|
|
373
|
-
return;
|
|
374
|
-
} catch (error) {
|
|
375
|
-
errors.push(`${attempt.command}: ${error?.message || error}`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
throw new Error(
|
|
380
|
-
`Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
|
|
385
|
-
console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
|
|
386
|
-
runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
|
|
387
|
-
if (!pythonCanImport(pythonCmd, "langfuse")) {
|
|
388
|
-
throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
|
|
389
|
-
}
|
|
390
|
-
return pythonCmd;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
|
|
346
|
+
function pythonExecutableInVenv(venvDir) {
|
|
347
|
+
return process.platform === "win32"
|
|
348
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
349
|
+
: path.join(venvDir, "bin", "python");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function pythonCanImport(pythonCmd, moduleName) {
|
|
353
|
+
try {
|
|
354
|
+
execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
|
|
355
|
+
return true;
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
|
|
362
|
+
const attempts = [
|
|
363
|
+
{ command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
|
|
364
|
+
{ command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
|
|
365
|
+
{ command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
const errors = [];
|
|
369
|
+
for (const attempt of attempts) {
|
|
370
|
+
try {
|
|
371
|
+
console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
|
|
372
|
+
execFileSync(attempt.command, attempt.args, { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
|
|
373
|
+
return;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
errors.push(`${attempt.command}: ${error?.message || error}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
|
|
385
|
+
console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
|
|
386
|
+
runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
|
|
387
|
+
if (!pythonCanImport(pythonCmd, "langfuse")) {
|
|
388
|
+
throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
|
|
389
|
+
}
|
|
390
|
+
return pythonCmd;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
|
|
394
394
|
if (process.platform === "win32") {
|
|
395
395
|
const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
|
|
396
396
|
const content = [
|
|
@@ -398,7 +398,7 @@ async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
|
|
|
398
398
|
`${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
|
|
399
399
|
""
|
|
400
400
|
].join(os.EOL);
|
|
401
|
-
await fsp.writeFile(launcher, content, "utf8");
|
|
401
|
+
await fsp.writeFile(launcher, "\ufeff" + content, "utf8");
|
|
402
402
|
return launcher;
|
|
403
403
|
}
|
|
404
404
|
|
|
@@ -420,14 +420,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
420
420
|
|
|
421
421
|
if (!fs.existsSync(venvPython)) {
|
|
422
422
|
console.log(`Creating Python virtual environment: ${venvDir}`);
|
|
423
|
-
try {
|
|
424
|
-
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
|
|
425
|
-
} catch (e) {
|
|
426
|
-
if (process.platform !== "win32") {
|
|
427
|
-
return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
|
|
428
|
-
}
|
|
429
|
-
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
430
|
-
}
|
|
423
|
+
try {
|
|
424
|
+
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
|
|
425
|
+
} catch (e) {
|
|
426
|
+
if (process.platform !== "win32") {
|
|
427
|
+
return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
|
|
428
|
+
}
|
|
429
|
+
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
430
|
+
}
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
console.log("Installing/updating Python package in venv: langfuse");
|
|
@@ -435,14 +435,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
435
435
|
execFileSync(
|
|
436
436
|
venvPython,
|
|
437
437
|
["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
|
|
438
|
-
{ stdio: "inherit" }
|
|
439
|
-
);
|
|
440
|
-
} catch (e) {
|
|
441
|
-
if (process.platform !== "win32") {
|
|
442
|
-
return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
|
|
443
|
-
}
|
|
444
|
-
throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
|
|
445
|
-
}
|
|
438
|
+
{ stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
|
|
439
|
+
);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
if (process.platform !== "win32") {
|
|
442
|
+
return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
|
|
443
|
+
}
|
|
444
|
+
throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
|
|
445
|
+
}
|
|
446
446
|
|
|
447
447
|
return venvPython;
|
|
448
448
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { resolveOpencodeCli } from "./resolve-opencode-cli.mjs";
|
|
5
|
-
|
|
6
|
-
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
7
|
-
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
8
|
-
|
|
9
|
-
function normalizeUserId(v) {
|
|
10
|
-
return String(v || "").trim();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function assertValidUserId(userId) {
|
|
14
|
-
if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
|
|
15
|
-
throw new Error(`User ID must match ${USER_ID_PATTERN_TEXT}, for example h00613222 or hwx1234567.`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { resolveOpencodeCli } from "./resolve-opencode-cli.mjs";
|
|
5
|
+
|
|
6
|
+
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
7
|
+
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
8
|
+
|
|
9
|
+
function normalizeUserId(v) {
|
|
10
|
+
return String(v || "").trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function assertValidUserId(userId) {
|
|
14
|
+
if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
|
|
15
|
+
throw new Error(`User ID must match ${USER_ID_PATTERN_TEXT}, for example h00613222 or hwx1234567.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
18
|
|
|
19
19
|
function parseArgs(argv) {
|
|
20
20
|
const args = {};
|
|
@@ -31,7 +31,7 @@ function parseArgs(argv) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function runNodeScript(scriptPath, extraArgs = []) {
|
|
34
|
-
const r = spawnSync(process.execPath, [scriptPath, ...extraArgs], { stdio: "inherit" });
|
|
34
|
+
const r = spawnSync(process.execPath, [scriptPath, ...extraArgs], { stdio: "inherit", encoding: "utf8" });
|
|
35
35
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -42,12 +42,12 @@ function main() {
|
|
|
42
42
|
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
43
43
|
const secretKey =
|
|
44
44
|
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
45
|
-
const baseUrl = args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || "http://120.46.221.227:3000";
|
|
46
|
-
const userId = normalizeUserId(args.userId || args.userid || "");
|
|
47
|
-
if (!userId) {
|
|
48
|
-
throw new Error("Missing userId. Run with --userId=your-id.");
|
|
49
|
-
}
|
|
50
|
-
assertValidUserId(userId);
|
|
45
|
+
const baseUrl = args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || "http://120.46.221.227:3000";
|
|
46
|
+
const userId = normalizeUserId(args.userId || args.userid || "");
|
|
47
|
+
if (!userId) {
|
|
48
|
+
throw new Error("Missing userId. Run with --userId=your-id.");
|
|
49
|
+
}
|
|
50
|
+
assertValidUserId(userId);
|
|
51
51
|
|
|
52
52
|
// 1) 先执行 setup:默认会写入 Windows 用户级 LANGFUSE_*(从桌面/开始菜单启动 OpenCode 也能读到)
|
|
53
53
|
const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -89,7 +89,7 @@ function main() {
|
|
|
89
89
|
|
|
90
90
|
console.log(`启动 OpenCode:${resolved} ${cmdArgs.join(" ")}`.trim() + ". userId: " + (userId || "<none>"));
|
|
91
91
|
const useShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(resolved);
|
|
92
|
-
const r = spawnSync(resolved, cmdArgs, { stdio: "inherit", env, shell: useShell });
|
|
92
|
+
const r = spawnSync(resolved, cmdArgs, { stdio: "inherit", encoding: "utf8", env, shell: useShell });
|
|
93
93
|
if (r.error?.code === "ENOENT") {
|
|
94
94
|
console.error(
|
|
95
95
|
`无法执行 "${resolved}"。若这是 .cmd,请尝试:npm run opencode:langfuse:run -- --cmd=${path.join(
|
|
@@ -510,45 +510,45 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
510
510
|
" const processor = new LangfuseSpanProcessor({ publicKey, secretKey, baseUrl, environment });",
|
|
511
511
|
" const spanProcessors = [processor, createMetricsSpanProcessor(userId)];",
|
|
512
512
|
" if (userId) spanProcessors.push(createUserIdSpanProcessor(userId));",
|
|
513
|
-
"",
|
|
514
|
-
" const sdk = new NodeSDK({ spanProcessors });",
|
|
515
|
-
" const sdkStartPromise = Promise.resolve().then(() => sdk.start()).catch((err) => {",
|
|
516
|
-
' log("warn", `OTEL SDK start failed: ${err?.message ?? err}`);',
|
|
517
|
-
" });",
|
|
518
|
-
" const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
|
|
519
|
-
" const knownSkillNames = await collectKnownSkillNames();",
|
|
520
|
-
" const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
|
|
513
|
+
"",
|
|
514
|
+
" const sdk = new NodeSDK({ spanProcessors });",
|
|
515
|
+
" const sdkStartPromise = Promise.resolve().then(() => sdk.start()).catch((err) => {",
|
|
516
|
+
' log("warn", `OTEL SDK start failed: ${err?.message ?? err}`);',
|
|
517
|
+
" });",
|
|
518
|
+
" const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
|
|
519
|
+
" const knownSkillNames = await collectKnownSkillNames();",
|
|
520
|
+
" const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
|
|
521
521
|
" const messageTextById = new Map();",
|
|
522
522
|
" const skillUsagesByMessageId = new Map();",
|
|
523
523
|
" const skillUsagesBySessionId = new Map();",
|
|
524
524
|
" const startupSkillSessionIds = new Set();",
|
|
525
525
|
" const toolCallIdsByMessageId = new Map();",
|
|
526
526
|
" const toolCallIdsBySessionId = new Map();",
|
|
527
|
-
" const toolResultIdsByMessageId = new Map();",
|
|
528
|
-
" const toolResultIdsBySessionId = new Map();",
|
|
529
|
-
" const emittedMessageIds = new Set();",
|
|
530
|
-
"",
|
|
527
|
+
" const toolResultIdsByMessageId = new Map();",
|
|
528
|
+
" const toolResultIdsBySessionId = new Map();",
|
|
529
|
+
" const emittedMessageIds = new Set();",
|
|
530
|
+
"",
|
|
531
531
|
' log("info", `OTEL tracing initialized -> ${baseUrl}`);',
|
|
532
532
|
' if (userId) log("info", `LANGFUSE userId configured -> ${userId}`);',
|
|
533
533
|
' if (knownSkillNames.length) log("info", `OpenCode skills discovered -> ${knownSkillNames.length}`);',
|
|
534
534
|
"",
|
|
535
|
-
" let shutdownStarted = false;",
|
|
536
|
-
" const flush = async (reason) => {",
|
|
537
|
-
" try {",
|
|
538
|
-
' log("info", `Flushing OTEL spans on ${reason}`);',
|
|
539
|
-
" await sdkStartPromise;",
|
|
540
|
-
" await processor.forceFlush();",
|
|
535
|
+
" let shutdownStarted = false;",
|
|
536
|
+
" const flush = async (reason) => {",
|
|
537
|
+
" try {",
|
|
538
|
+
' log("info", `Flushing OTEL spans on ${reason}`);',
|
|
539
|
+
" await sdkStartPromise;",
|
|
540
|
+
" await processor.forceFlush();",
|
|
541
541
|
" } catch (error) {",
|
|
542
542
|
' log("warn", `OTEL forceFlush failed on ${reason}: ${error?.message ?? error}`);',
|
|
543
543
|
" }",
|
|
544
544
|
" };",
|
|
545
545
|
" const shutdown = async (reason) => {",
|
|
546
|
-
" if (shutdownStarted) return;",
|
|
547
|
-
" shutdownStarted = true;",
|
|
548
|
-
" try {",
|
|
549
|
-
' log("info", `Shutting down OTEL SDK on ${reason}`);',
|
|
550
|
-
" await sdkStartPromise;",
|
|
551
|
-
" await sdk.shutdown();",
|
|
546
|
+
" if (shutdownStarted) return;",
|
|
547
|
+
" shutdownStarted = true;",
|
|
548
|
+
" try {",
|
|
549
|
+
' log("info", `Shutting down OTEL SDK on ${reason}`);',
|
|
550
|
+
" await sdkStartPromise;",
|
|
551
|
+
" await sdk.shutdown();",
|
|
552
552
|
" } catch (error) {",
|
|
553
553
|
' log("warn", `OTEL shutdown failed on ${reason}: ${error?.message ?? error}`);',
|
|
554
554
|
" }",
|
|
@@ -586,7 +586,7 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
586
586
|
" }",
|
|
587
587
|
" set.add(activity.toolCallId || `${kind}:${set.size + 1}`);",
|
|
588
588
|
" };",
|
|
589
|
-
" const recordInteractionMetric = async (event) => {",
|
|
589
|
+
" const recordInteractionMetric = async (event) => {",
|
|
590
590
|
" const payload = eventPayload(event);",
|
|
591
591
|
" const part = eventPart(event);",
|
|
592
592
|
" const partType = part?.type ?? '';",
|
|
@@ -617,8 +617,8 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
617
617
|
" const tokenMetrics = tokenMetricsFromPart(part);",
|
|
618
618
|
" const total = tokenMetrics.total ?? (tokenMetrics.input !== undefined && tokenMetrics.output !== undefined ? tokenMetrics.input + tokenMetrics.output : undefined);",
|
|
619
619
|
" const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
|
|
620
|
-
" await sdkStartPromise;",
|
|
621
|
-
" const span = getMetricsTracer().startSpan('OpenCode Agent Turn');",
|
|
620
|
+
" await sdkStartPromise;",
|
|
621
|
+
" const span = getMetricsTracer().startSpan('OpenCode Agent Turn');",
|
|
622
622
|
" const text = messageTextById.get(messageId) || '';",
|
|
623
623
|
" const skillUsages = dedupeSkillUsages([...(skillUsagesByMessageId.get(messageId) ?? []), ...(skillUsagesBySessionId.get(sessionId) ?? [])]);",
|
|
624
624
|
" const interactionId = `opencode:${userId || \"unknown\"}:${sessionId || \"unknown\"}:${messageId}`;",
|
|
@@ -674,9 +674,9 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
674
674
|
' log("warn", "OpenTelemetry experimental feature is disabled in Opencode config - tracing disabled");',
|
|
675
675
|
" }",
|
|
676
676
|
" },",
|
|
677
|
-
" event: async ({ event }) => {",
|
|
678
|
-
" const eventType = pickEventString(event?.name, event?.type, eventPayload(event)?.name, eventPayload(event)?.type);",
|
|
679
|
-
" await recordInteractionMetric(event);",
|
|
677
|
+
" event: async ({ event }) => {",
|
|
678
|
+
" const eventType = pickEventString(event?.name, event?.type, eventPayload(event)?.name, eventPayload(event)?.type);",
|
|
679
|
+
" await recordInteractionMetric(event);",
|
|
680
680
|
' if (eventType === "session.idle" || eventType === "message.updated" || eventType === "message.part.updated" || eventType === "session.updated" || eventType === "session.idle.1" || eventType === "message.updated.1" || eventType === "message.part.updated.1" || eventType === "session.updated.1") {',
|
|
681
681
|
" await flush(eventType);",
|
|
682
682
|
" }",
|
|
@@ -719,7 +719,7 @@ function writeWindowsLauncherCmd(opencodeDir, { publicKey, secretKey, baseUrl, u
|
|
|
719
719
|
cmd.push(" exit /b %ERRORLEVEL%");
|
|
720
720
|
cmd.push(")");
|
|
721
721
|
cmd.push("opencode %*");
|
|
722
|
-
fs.writeFileSync(p, cmd.join("\r\n") + "\r\n", "utf8");
|
|
722
|
+
fs.writeFileSync(p, "\ufeff" + cmd.join("\r\n") + "\r\n", "utf8");
|
|
723
723
|
return p;
|
|
724
724
|
}
|
|
725
725
|
|
|
@@ -754,7 +754,7 @@ function writeAutoUpdateHelper(target) {
|
|
|
754
754
|
"exit /b 0",
|
|
755
755
|
""
|
|
756
756
|
];
|
|
757
|
-
fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
|
|
757
|
+
fs.writeFileSync(helper, "\ufeff" + lines.join(os.EOL), "utf8");
|
|
758
758
|
return helper;
|
|
759
759
|
}
|
|
760
760
|
|
|
@@ -802,7 +802,7 @@ function writeWindowsUpdateCheckScript(dir) {
|
|
|
802
802
|
"}",
|
|
803
803
|
""
|
|
804
804
|
].join("\r\n");
|
|
805
|
-
fs.writeFileSync(p, ps, "utf8");
|
|
805
|
+
fs.writeFileSync(p, "\ufeff" + ps, "utf8");
|
|
806
806
|
return p;
|
|
807
807
|
}
|
|
808
808
|
|
|
@@ -820,7 +820,7 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
|
|
|
820
820
|
cmd.push(windowsAutoUpdateCommand("opencode"));
|
|
821
821
|
cmd.push(`call ${cmdQuote(realOpencodeCli)} %*`);
|
|
822
822
|
cmd.push("exit /b %ERRORLEVEL%");
|
|
823
|
-
fs.writeFileSync(shim, cmd.join("\r\n") + "\r\n", "utf8");
|
|
823
|
+
fs.writeFileSync(shim, "\ufeff" + cmd.join("\r\n") + "\r\n", "utf8");
|
|
824
824
|
return { shim, shimDir };
|
|
825
825
|
}
|
|
826
826
|
|
|
@@ -925,17 +925,32 @@ function printNpmDiagnostics() {
|
|
|
925
925
|
} catch (_) {}
|
|
926
926
|
}
|
|
927
927
|
|
|
928
|
-
|
|
928
|
+
/**
|
|
929
|
+
* 重置终端状态,防止子进程异常退出后遗留 ANSI 转义序列。
|
|
930
|
+
*/
|
|
931
|
+
function resetTerminalState() {
|
|
932
|
+
if (process.stdout.isTTY) {
|
|
933
|
+
// \x1b[0m - 重置所有颜色和属性
|
|
934
|
+
// \x1b[?25h - 显示光标(防止 npm 隐藏光标后未恢复)
|
|
935
|
+
// \x1b[r - 重置滚动区域
|
|
936
|
+
process.stdout.write('\x1b[0m\x1b[?25h\x1b[r');
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function runNpmInstallCapture(npmArgs, options = {}) {
|
|
929
941
|
return new Promise((resolve) => {
|
|
930
942
|
const cliJs = getNpmCliJsPath();
|
|
931
943
|
const npmExe = getNpmExecutable();
|
|
932
944
|
const command = fs.existsSync(cliJs) ? process.execPath : npmExe;
|
|
933
945
|
const args = fs.existsSync(cliJs) ? [cliJs, ...npmArgs] : npmArgs;
|
|
934
946
|
const useShell = process.platform === "win32" && !fs.existsSync(cliJs) && /\.(cmd|bat)$/i.test(npmExe);
|
|
947
|
+
const timeoutMs = options.timeoutMs ?? 600000; // 默认 10 分钟超时
|
|
948
|
+
|
|
935
949
|
const child = spawn(command, args, {
|
|
936
950
|
encoding: "utf8",
|
|
937
951
|
shell: useShell,
|
|
938
|
-
windowsHide: true
|
|
952
|
+
windowsHide: true,
|
|
953
|
+
timeout: timeoutMs
|
|
939
954
|
});
|
|
940
955
|
let stdout = "";
|
|
941
956
|
let stderr = "";
|
|
@@ -960,12 +975,23 @@ function runNpmInstallCapture(npmArgs) {
|
|
|
960
975
|
});
|
|
961
976
|
child.on("error", (error) => {
|
|
962
977
|
clearInterval(timer);
|
|
978
|
+
resetTerminalState();
|
|
963
979
|
resolve({ status: null, stdout, stderr, error });
|
|
964
980
|
});
|
|
965
981
|
child.on("close", (status) => {
|
|
966
982
|
clearInterval(timer);
|
|
983
|
+
if (status !== 0) {
|
|
984
|
+
resetTerminalState();
|
|
985
|
+
}
|
|
967
986
|
resolve({ status, stdout, stderr });
|
|
968
987
|
});
|
|
988
|
+
child.on("timeout", () => {
|
|
989
|
+
clearInterval(timer);
|
|
990
|
+
console.error("");
|
|
991
|
+
console.error(`npm install timed out after ${Math.round(timeoutMs / 1000)}s, killing process...`);
|
|
992
|
+
resetTerminalState();
|
|
993
|
+
child.kill('SIGTERM');
|
|
994
|
+
});
|
|
969
995
|
});
|
|
970
996
|
}
|
|
971
997
|
|
|
@@ -974,29 +1000,78 @@ function npmInstallLooksLikeMissingVersion(result) {
|
|
|
974
1000
|
return text.includes("etarget") || text.includes("notarget") || text.includes("no matching version found");
|
|
975
1001
|
}
|
|
976
1002
|
|
|
977
|
-
function isOfficialNpmRegistry(registry) {
|
|
978
|
-
return /^https?:\/\/registry\.npmjs\.org\/?$/i.test(String(registry || "").trim());
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1003
|
+
function isOfficialNpmRegistry(registry) {
|
|
1004
|
+
return /^https?:\/\/registry\.npmjs\.org\/?$/i.test(String(registry || "").trim());
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
1008
|
+
const DEFAULT_NPM_REGISTRY = "https://registry.npmmirror.com/";
|
|
1009
|
+
|
|
1010
|
+
function isChinaNetwork() {
|
|
1011
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
|
|
1012
|
+
return tz.includes("Shanghai") || tz.includes("Chongqing") || tz.includes("Harbin");
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async function checkInstalledVersion(opencodeDir, pkgName) {
|
|
1016
|
+
const pkgJsonPath = path.join(opencodeDir, "node_modules", pkgName, "package.json");
|
|
1017
|
+
if (!fs.existsSync(pkgJsonPath)) return null;
|
|
1018
|
+
try {
|
|
1019
|
+
const txt = stripBom(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
1020
|
+
if (!txt.trim()) return null;
|
|
1021
|
+
const installed = parseJsonRelaxed(txt, pkgJsonPath);
|
|
1022
|
+
return installed?.version || null;
|
|
1023
|
+
} catch {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-langfuse", npmRegistry = "" }) {
|
|
1029
|
+
const installedVersion = await checkInstalledVersion(opencodeDir, pkgName);
|
|
1030
|
+
const currentVersion = packageJson.version;
|
|
1031
|
+
|
|
1032
|
+
if (installedVersion === currentVersion) {
|
|
1033
|
+
console.log(`插件版本 ${currentVersion} 已安装,跳过 npm install ...`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (installedVersion) {
|
|
1038
|
+
console.log(`插件版本 ${installedVersion} 已安装,升级到 ${currentVersion} ...`);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const isOfficial = npmRegistry && isOfficialNpmRegistry(npmRegistry);
|
|
1042
|
+
const effectiveRegistry = npmRegistry || (isChinaNetwork() ? DEFAULT_NPM_REGISTRY : OFFICIAL_NPM_REGISTRY);
|
|
1043
|
+
|
|
1044
|
+
const npmArgs = [
|
|
1045
|
+
"install", pkgName, "--prefix", opencodeDir,
|
|
1046
|
+
"--package-lock=false", "--no-save",
|
|
1047
|
+
"--audit=false", "--fund=false",
|
|
1048
|
+
"--prefer-offline",
|
|
1049
|
+
"--registry", effectiveRegistry
|
|
1050
|
+
];
|
|
1051
|
+
|
|
1052
|
+
const cliJs = getNpmCliJsPath();
|
|
1053
|
+
console.log(`使用 npm:${fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable()}`);
|
|
1054
|
+
console.log(`使用 npm registry:${effectiveRegistry}`);
|
|
1055
|
+
console.log("Installing OpenCode Langfuse plugin. This can take a few minutes on slow networks...");
|
|
1056
|
+
|
|
1057
|
+
let r = await runNpmInstallCapture(npmArgs, { timeoutMs: 600000 });
|
|
1058
|
+
|
|
1059
|
+
if (!r.error && r.status !== 0 && npmInstallLooksLikeMissingVersion(r) && !isOfficialNpmRegistry(effectiveRegistry)) {
|
|
1060
|
+
console.error("");
|
|
1061
|
+
console.error(`npm registry appears to be missing a package version. Retrying with ${OFFICIAL_NPM_REGISTRY} ...`);
|
|
1062
|
+
const retryArgs = [
|
|
1063
|
+
"install", pkgName, "--prefix", opencodeDir,
|
|
1064
|
+
"--package-lock=false", "--no-save",
|
|
1065
|
+
"--audit=false", "--fund=false",
|
|
1066
|
+
"--prefer-offline",
|
|
1067
|
+
"--registry", OFFICIAL_NPM_REGISTRY
|
|
1068
|
+
];
|
|
1069
|
+
r = await runNpmInstallCapture(retryArgs, { timeoutMs: 600000 });
|
|
1070
|
+
if (!r.error && r.status === 0) return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
999
1073
|
if (!r.error && r.status === 0) return;
|
|
1074
|
+
|
|
1000
1075
|
printNpmDiagnostics();
|
|
1001
1076
|
const npmLabel = fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable();
|
|
1002
1077
|
throw new Error(
|
|
@@ -1005,20 +1080,20 @@ async function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-la
|
|
|
1005
1080
|
);
|
|
1006
1081
|
}
|
|
1007
1082
|
|
|
1008
|
-
function setWindowsUserEnv({ publicKey, secretKey, baseUrl }) {
|
|
1083
|
+
function setWindowsUserEnv({ publicKey, secretKey, baseUrl }) {
|
|
1009
1084
|
const cmd = [
|
|
1010
1085
|
"$ErrorActionPreference = 'Stop';",
|
|
1011
1086
|
`[Environment]::SetEnvironmentVariable('LANGFUSE_PUBLIC_KEY', ${psQuote(publicKey)}, 'User');`,
|
|
1012
1087
|
`[Environment]::SetEnvironmentVariable('LANGFUSE_SECRET_KEY', ${psQuote(secretKey)}, 'User');`,
|
|
1013
1088
|
`[Environment]::SetEnvironmentVariable('LANGFUSE_BASEURL', ${psQuote(baseUrl)}, 'User');`
|
|
1014
|
-
|
|
1015
|
-
const r = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
1089
|
+
].join(" ");
|
|
1090
|
+
const r = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", encoding: "utf8" });
|
|
1016
1091
|
if (r.status !== 0) {
|
|
1017
|
-
throw new Error("
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function prependWindowsUserPath(dir) {
|
|
1092
|
+
throw new Error("写入用户级环境变量失败。");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function prependWindowsUserPath(dir) {
|
|
1022
1097
|
if (process.platform !== "win32") return false;
|
|
1023
1098
|
const cmd = [
|
|
1024
1099
|
"$ErrorActionPreference = 'Stop';",
|
|
@@ -1033,7 +1108,7 @@ function prependWindowsUserPath(dir) {
|
|
|
1033
1108
|
" [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
|
|
1034
1109
|
"}"
|
|
1035
1110
|
].join(" ");
|
|
1036
|
-
const r = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
1111
|
+
const r = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", encoding: "utf8" });
|
|
1037
1112
|
if (r.status !== 0) {
|
|
1038
1113
|
throw new Error("写入用户 PATH 失败:无法把 OpenCode Langfuse shim 加入 PATH。");
|
|
1039
1114
|
}
|
|
@@ -1110,7 +1185,7 @@ async function main() {
|
|
|
1110
1185
|
throw new Error("缺少参数:--userId=你的工号");
|
|
1111
1186
|
}
|
|
1112
1187
|
assertValidUserId(userId);
|
|
1113
|
-
const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || "";
|
|
1188
|
+
const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || "";
|
|
1114
1189
|
|
|
1115
1190
|
const home = os.homedir();
|
|
1116
1191
|
const opencodeDir = path.join(home, ".config", "opencode");
|
|
@@ -1213,7 +1288,7 @@ async function main() {
|
|
|
1213
1288
|
"$ErrorActionPreference = 'Stop';",
|
|
1214
1289
|
`[Environment]::SetEnvironmentVariable('LANGFUSE_USER_ID', ${psQuote(userId)}, 'User');`
|
|
1215
1290
|
].join(" ");
|
|
1216
|
-
spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
1291
|
+
spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", encoding: "utf8" });
|
|
1217
1292
|
}
|
|
1218
1293
|
if (opencodeShim?.shimDir) {
|
|
1219
1294
|
prependWindowsUserPath(opencodeShim.shimDir);
|