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.
@@ -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
- function runNpmInstallCapture(npmArgs) {
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
- async function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-langfuse", npmRegistry = "" }) {
984
- const npmArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false"];
985
- const effectiveRegistry = npmRegistry || OFFICIAL_NPM_REGISTRY;
986
- npmArgs.push("--registry", effectiveRegistry);
987
- const cliJs = getNpmCliJsPath();
988
- console.log(`使用 npm:${fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable()}`);
989
- console.log(`使用 npm registry:${effectiveRegistry}`);
990
- console.log("Installing OpenCode Langfuse plugin. This can take a few minutes on slow networks...");
991
- let r = await runNpmInstallCapture(npmArgs);
992
- if (!r.error && r.status !== 0 && npmInstallLooksLikeMissingVersion(r) && !isOfficialNpmRegistry(effectiveRegistry)) {
993
- console.error("");
994
- console.error(`npm registry appears to be missing a package version. Retrying with ${OFFICIAL_NPM_REGISTRY} ...`);
995
- const retryArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false", "--registry", OFFICIAL_NPM_REGISTRY];
996
- r = await runNpmInstallCapture(retryArgs);
997
- if (!r.error && r.status === 0) return;
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
- ].join(" ");
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("写入用户级环境变量失败(PowerShell SetEnvironmentVariable)。");
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);