oh-langfuse 0.1.16 → 0.1.18

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 CHANGED
@@ -9,7 +9,7 @@ Claude Code、OpenCode 和 Codex。它提供终端安装向导、直接 setup/ch
9
9
  本仓是 AI Coding 工具链中的 Langfuse 能力包,负责:
10
10
 
11
11
  - 检查 Node.js、npm、Python、pip、OpenCode CLI 等本地依赖;
12
- - 收集 Langfuse 用户标识,通常是员工号,例如 `h00613222`;
12
+ - 收集 Langfuse 用户标识,必须匹配 `^[a-z](?:\d{8}|wx\d{7})$`,例如 `h00613222` 或 `hwx1234567`;
13
13
  - 安装或更新 hook 脚本、插件文件、Python 虚拟环境和用户级配置;
14
14
  - 校验 Claude Code、OpenCode、Codex 的 Langfuse 配置是否生效。
15
15
 
@@ -109,19 +109,18 @@ Langfuse 配置启动 OpenCode。安装器可以读取带注释或尾随逗号
109
109
  此时配置会写入 WSL 用户自己的 `$HOME/.config/opencode`:
110
110
 
111
111
  ```bash
112
- npx oh-langfuse@latest setup opencode --userId=h00613222
112
+ npx oh-langfuse@latest setup opencode --userId=h00613222 --yes
113
113
  npx oh-langfuse@latest check opencode
114
114
  ```
115
115
 
116
+ 安装完成后会自动执行一次 `check opencode`。如果检查提示当前终端缺少
117
+ `LANGFUSE_*`,请新开一个 WSL 终端,或执行安装器提示的 `source ~/.bashrc`
118
+ / `source ~/.zshrc`,也可以直接使用生成的
119
+ `~/.config/opencode/launch-opencode-langfuse.sh` 启动 OpenCode。
120
+
116
121
  如果 `opencode.json` 原本是 JSONC 风格(例如带注释或尾随逗号),安装器会先兼容读取,
117
122
  再写回标准 JSON。
118
123
 
119
- 如果你确实是在 Windows PowerShell 里远程配置 WSL,才需要使用 `--wsl`:
120
-
121
- ```bash
122
- npx oh-langfuse@latest setup opencode --wsl=Ubuntu --userId=h00613222
123
- ```
124
-
125
124
  ### Codex
126
125
 
127
126
  安装 `codex_langfuse_notify.py`,创建 `~/.codex/langfuse-venv`,安装 Python
package/bin/cli.js CHANGED
@@ -8,9 +8,11 @@ import { spawnSync } from "node:child_process";
8
8
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
9
  const scriptsDir = path.join(rootDir, "scripts");
10
10
 
11
- const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
12
- const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
13
- const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
11
+ const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
12
+ const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
13
+ const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
14
+ const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
15
+ const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
14
16
 
15
17
  const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
16
18
  const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
@@ -81,9 +83,21 @@ function mask(v) {
81
83
  return `${v.slice(0, 4)}...${v.slice(-4)}`;
82
84
  }
83
85
 
84
- function hasValue(v) {
85
- return typeof v === "string" && v.trim().length > 0;
86
- }
86
+ function hasValue(v) {
87
+ return typeof v === "string" && v.trim().length > 0;
88
+ }
89
+
90
+ function normalizeUserId(v) {
91
+ return String(v || "").trim();
92
+ }
93
+
94
+ function isValidUserId(v) {
95
+ return USER_ID_PATTERN.test(normalizeUserId(v));
96
+ }
97
+
98
+ function userIdValidationMessage() {
99
+ return `User ID must match ${USER_ID_PATTERN_TEXT}, for example h00613222 or hwx1234567.`;
100
+ }
87
101
 
88
102
  function scriptPath(name) {
89
103
  return path.join(scriptsDir, name);
@@ -249,17 +263,25 @@ function runNodeScript(name, args = [], { dryRun = false } = {}) {
249
263
  return r.status ?? (r.error ? 1 : 0);
250
264
  }
251
265
 
252
- async function askText(rl, label, { defaultValue = "", required = false } = {}) {
253
- while (true) {
254
- const suffix = defaultValue ? paint(` ${defaultValue}`, t.muted) : "";
255
- const answer = (await rl.question(`${paint(label, t.cyan)}${suffix}\n${paint(">", t.teal)} `)).trim();
256
- const value = answer || defaultValue;
257
- if (!required || hasValue(value)) return value;
258
- console.log(paint("This value is required.", t.red));
259
- }
260
- }
261
-
262
- async function askYesNo(rl, label, { defaultValue = true } = {}) {
266
+ async function askText(rl, label, { defaultValue = "", required = false, validate = null, invalidMessage = "" } = {}) {
267
+ while (true) {
268
+ const suffix = defaultValue ? paint(` ${defaultValue}`, t.muted) : "";
269
+ const answer = (await rl.question(`${paint(label, t.cyan)}${suffix}\n${paint(">", t.teal)} `)).trim();
270
+ const value = answer || defaultValue;
271
+ if (required && !hasValue(value)) {
272
+ console.log(paint("This value is required.", t.red));
273
+ continue;
274
+ }
275
+ if (validate && hasValue(value) && !validate(value)) {
276
+ console.log(paint(invalidMessage || "Invalid value.", t.red));
277
+ continue;
278
+ }
279
+ if (!required || hasValue(value)) return value;
280
+ console.log(paint("This value is required.", t.red));
281
+ }
282
+ }
283
+
284
+ async function askYesNo(rl, label, { defaultValue = false } = {}) {
263
285
  const hint = defaultValue ? "Y/n" : "y/N";
264
286
  while (true) {
265
287
  const answer = (await rl.question(`${paint(label, t.cyan)} ${paint(`(${hint})`, t.muted)} `)).trim().toLowerCase();
@@ -451,34 +473,47 @@ async function askMultiChoice(rl, label, choices, options = {}) {
451
473
  .map((idx) => choices[idx].value);
452
474
  }
453
475
 
454
- function langfuseConfig() {
455
- return {
456
- baseUrl: envDefault("LANGFUSE_BASEURL", envDefault("LANGFUSE_HOST", DEFAULT_LANGFUSE_BASE_URL)),
457
- publicKey: envDefault("LANGFUSE_PUBLIC_KEY", DEFAULT_LANGFUSE_PUBLIC_KEY),
458
- secretKey: envDefault("LANGFUSE_SECRET_KEY", DEFAULT_LANGFUSE_SECRET_KEY),
459
- userId: envDefault("LANGFUSE_USER_ID", envDefault("CC_USER_ID", ""))
460
- };
461
- }
462
-
463
- async function collectLangfuseConfig(rl, { requireUserId = false } = {}) {
464
- const config = langfuseConfig();
465
- renderSection("Langfuse Target", [
466
- labelValue("Base URL", config.baseUrl, t.teal),
467
- labelValue("Public Key", config.publicKey, t.blue),
468
- labelValue("Secret Key", "configured", t.teal)
469
- ]);
476
+ function langfuseConfig(overrides = {}) {
477
+ const config = {
478
+ baseUrl: envDefault("LANGFUSE_BASEURL", envDefault("LANGFUSE_HOST", DEFAULT_LANGFUSE_BASE_URL)),
479
+ publicKey: envDefault("LANGFUSE_PUBLIC_KEY", DEFAULT_LANGFUSE_PUBLIC_KEY),
480
+ secretKey: envDefault("LANGFUSE_SECRET_KEY", DEFAULT_LANGFUSE_SECRET_KEY),
481
+ userId: ""
482
+ };
483
+ for (const [key, value] of Object.entries(overrides)) {
484
+ if (hasValue(value)) config[key] = value;
485
+ }
486
+ return config;
487
+ }
488
+
489
+ async function collectLangfuseConfig(rl, { requireUserId = false, overrides = {} } = {}) {
490
+ const config = langfuseConfig(overrides);
491
+ renderSection("Langfuse Target", [
492
+ labelValue("Base URL", config.baseUrl, t.teal),
493
+ labelValue("Public Key", config.publicKey, t.blue),
494
+ labelValue("Secret Key", "configured", t.teal)
495
+ ]);
496
+ if (hasValue(config.userId)) {
497
+ config.userId = normalizeUserId(config.userId);
498
+ if (!isValidUserId(config.userId)) {
499
+ throw new Error(userIdValidationMessage());
500
+ }
501
+ if (requireUserId) return config;
502
+ }
470
503
  config.userId = await askText(rl, "User ID / employee number, for example h00613222", {
471
- defaultValue: requireUserId ? "" : config.userId,
472
- required: requireUserId
504
+ defaultValue: "",
505
+ required: requireUserId,
506
+ validate: isValidUserId,
507
+ invalidMessage: userIdValidationMessage()
473
508
  });
474
509
  return config;
475
510
  }
476
511
 
477
- async function collectSharedConfig(rl, options) {
478
- clearScreen();
479
- renderBrand(options);
480
- return await collectLangfuseConfig(rl, { requireUserId: true });
481
- }
512
+ async function collectSharedConfig(rl, options) {
513
+ clearScreen();
514
+ renderBrand(options);
515
+ return await collectLangfuseConfig(rl, { requireUserId: true, overrides: options.configOverrides });
516
+ }
482
517
 
483
518
  function commonLangfuseArgs(config) {
484
519
  return [
@@ -493,27 +528,22 @@ function optionalInstallerArgs(options) {
493
528
  return [...(hasValue(options.pipIndexUrl) ? [`--pipIndexUrl=${options.pipIndexUrl}`] : [])];
494
529
  }
495
530
 
496
- function opencodePlatformArgs(options) {
497
- return [
498
- ...(options.wsl ? [typeof options.wsl === "string" ? `--wsl=${options.wsl}` : "--wsl"] : []),
499
- ...(hasValue(options.wslDistro) ? [`--wslDistro=${options.wslDistro}`] : [])
500
- ];
501
- }
502
-
503
- async function confirmAction(rl, title, rows, options) {
531
+ async function confirmAction(rl, title, rows, options) {
504
532
  clearScreen();
505
533
  renderBrand(options);
506
534
  renderSection(title, rows);
507
- if (options.dryRun) return true;
535
+ if (options.dryRun || options.yes) return true;
508
536
  console.log("");
509
- return await askYesNo(rl, "Continue with these changes", { defaultValue: true });
537
+ return await askYesNo(rl, "Continue with these changes", { defaultValue: false });
510
538
  }
511
539
 
512
- async function setupClaude(rl, options) {
513
- while (!(await ensureEnvironment(rl, "claude", options))) {}
514
- clearScreen();
515
- renderBrand(options);
516
- const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true }));
540
+ async function setupClaude(rl, options) {
541
+ if (!options.dryRun) {
542
+ while (!(await ensureEnvironment(rl, "claude", options))) {}
543
+ }
544
+ clearScreen();
545
+ renderBrand(options);
546
+ const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true, overrides: options.configOverrides }));
517
547
  const defaultHook = path.join(rootDir, "langfuse_hook.py");
518
548
  const pyPath = fs.existsSync(defaultHook) ? defaultHook : path.join(rootDir, "langfuse_hook.py");
519
549
 
@@ -528,26 +558,29 @@ async function setupClaude(rl, options) {
528
558
  labelValue("Python package", "langfuse", t.gold)
529
559
  ],
530
560
  options
531
- );
532
- if (!ok) return 0;
533
- return runNodeScript("langfuse-setup.mjs", args, options);
534
- }
535
-
561
+ );
562
+ if (!ok) return 0;
563
+ const code = runNodeScript("langfuse-setup.mjs", args, options);
564
+ if (code === 0 && !options.dryRun && !options.skipCheck) return checkClaude(options, { clear: false });
565
+ return code;
566
+ }
567
+
536
568
  async function setupOpenCode(rl, options) {
537
- while (!(await ensureEnvironment(rl, "opencode", options))) {}
538
- clearScreen();
539
- renderBrand(options);
540
- const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true }));
541
- const setEnv = true;
542
- const installPlugin = true;
543
- const cliPath = "";
569
+ if (!options.dryRun) {
570
+ while (!(await ensureEnvironment(rl, "opencode", options))) {}
571
+ }
572
+ clearScreen();
573
+ renderBrand(options);
574
+ const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true, overrides: options.configOverrides }));
575
+ const setEnv = !options.noSetEnv;
576
+ const installPlugin = !options.skipPluginInstall;
577
+ const cliPath = options.cmd || "";
544
578
 
545
579
  const args = [
546
580
  ...commonLangfuseArgs(config),
547
581
  ...(hasValue(options.npmRegistry) ? [`--npmRegistry=${options.npmRegistry}`] : []),
548
- ...opencodePlatformArgs(options),
549
582
  ...(!setEnv ? ["--no-set-env"] : []),
550
- ...(!installPlugin ? ["--skip-plugin-install"] : []),
583
+ ...(!installPlugin ? ["--skip-plugin-install"] : []),
551
584
  ...(hasValue(cliPath) ? [`--cmd=${cliPath}`] : [])
552
585
  ];
553
586
  const ok = await confirmAction(
@@ -555,23 +588,27 @@ async function setupOpenCode(rl, options) {
555
588
  "OpenCode Langfuse Setup",
556
589
  [
557
590
  labelValue("User ID", config.userId || "<none>", config.userId ? t.teal : t.muted),
558
- labelValue("User env vars", setEnv ? "write" : "skip", setEnv ? t.teal : t.gold),
591
+ labelValue("User env vars", setEnv ? "write" : "skip", setEnv ? t.teal : t.gold),
559
592
  labelValue("Plugin install", installPlugin ? "install/update" : "skip", installPlugin ? t.teal : t.gold),
560
593
  labelValue("CLI path", cliPath || "auto-detect", t.blue),
561
- labelValue("Target runtime", options.wsl ? `WSL${typeof options.wsl === "string" ? ` (${options.wsl})` : ""}` : "current OS", t.violet),
594
+ labelValue("Target runtime", "current shell", t.violet),
562
595
  labelValue("Config file", "~/.config/opencode/opencode.json", t.violet)
563
596
  ],
564
597
  options
565
- );
566
- if (!ok) return 0;
567
- return runNodeScript("opencode-langfuse-setup.mjs", args, options);
568
- }
569
-
570
- async function setupCodex(rl, options) {
571
- while (!(await ensureEnvironment(rl, "codex", options))) {}
572
- clearScreen();
573
- renderBrand(options);
574
- const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true }));
598
+ );
599
+ if (!ok) return 0;
600
+ const code = runNodeScript("opencode-langfuse-setup.mjs", args, options);
601
+ if (code === 0 && !options.dryRun && !options.skipCheck) return checkOpenCode(options, { clear: false });
602
+ return code;
603
+ }
604
+
605
+ async function setupCodex(rl, options) {
606
+ if (!options.dryRun) {
607
+ while (!(await ensureEnvironment(rl, "codex", options))) {}
608
+ }
609
+ clearScreen();
610
+ renderBrand(options);
611
+ const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true, overrides: options.configOverrides }));
575
612
  const defaultHook = path.join(rootDir, "codex_langfuse_notify.py");
576
613
  const pyPath = fs.existsSync(defaultHook) ? defaultHook : path.join(rootDir, "codex_langfuse_notify.py");
577
614
 
@@ -586,10 +623,12 @@ async function setupCodex(rl, options) {
586
623
  labelValue("Python package", "langfuse", t.gold)
587
624
  ],
588
625
  options
589
- );
590
- if (!ok) return 0;
591
- return runNodeScript("codex-langfuse-setup.mjs", args, options);
592
- }
626
+ );
627
+ if (!ok) return 0;
628
+ const code = runNodeScript("codex-langfuse-setup.mjs", args, options);
629
+ if (code === 0 && !options.dryRun && !options.skipCheck) return checkCodex(options, { clear: false });
630
+ return code;
631
+ }
593
632
 
594
633
  function checkClaude(options, { clear = true } = {}) {
595
634
  if (clear) clearScreen();
@@ -600,7 +639,7 @@ function checkClaude(options, { clear = true } = {}) {
600
639
  function checkOpenCode(options, { clear = true } = {}) {
601
640
  if (clear) clearScreen();
602
641
  renderBrand(options);
603
- return runNodeScript("opencode-langfuse-check.mjs", opencodePlatformArgs(options), options);
642
+ return runNodeScript("opencode-langfuse-check.mjs", [], options);
604
643
  }
605
644
 
606
645
  function checkCodex(options, { clear = true } = {}) {
@@ -678,7 +717,7 @@ async function setupLangfuseMenu(rl, options) {
678
717
  rl,
679
718
  "Select Langfuse setup targets",
680
719
  [
681
- { label: "Claude Code Langfuse", value: "claude", selected: true, description: "Install the Langfuse Stop hook and connect Claude transcripts." },
720
+ { label: "Claude Code Langfuse", value: "claude", selected: false, description: "Install the Langfuse Stop hook and connect Claude transcripts." },
682
721
  { label: "OpenCode Langfuse", value: "opencode", selected: false, description: "Install the Langfuse plugin and enable OpenTelemetry." },
683
722
  { label: "Codex Langfuse", value: "codex", selected: false, description: "Install the Codex notify hook and connect session JSONL events." }
684
723
  ],
@@ -713,9 +752,14 @@ function printHelp() {
713
752
  ]);
714
753
  renderSection("Options", [
715
754
  `${paint("--dry-run", t.gold)} Preview actions without writing files or installing packages.`,
716
- `${paint("--wsl[=DISTRO]", t.gold)} Configure/check OpenCode inside WSL from Windows.`,
755
+ `${paint("--userId=ID", t.gold)} Provide the Langfuse user id without prompting.`,
756
+ `${paint("--langfuseBaseUrl=URL", t.gold)} Override the Langfuse base URL.`,
757
+ `${paint("--publicKey=KEY", t.gold)} Override the Langfuse public key.`,
758
+ `${paint("--secretKey=KEY", t.gold)} Override the Langfuse secret key.`,
759
+ `${paint("--yes", t.gold)} Accept setup changes without the confirmation prompt.`,
717
760
  `${paint("--npmRegistry=URL", t.gold)} Use a specific npm registry when installing the OpenCode plugin.`,
718
761
  `${paint("--pipIndexUrl=URL", t.gold)} Use a specific pip index for Python Langfuse installs.`,
762
+ `${paint("--skip-check", t.gold)} Do not run the target check after a successful setup.`,
719
763
  `${paint("--help", t.gold)} Show this help.`
720
764
  ]);
721
765
  }
@@ -724,10 +768,19 @@ async function main() {
724
768
  const args = parseArgs(process.argv.slice(2));
725
769
  const options = {
726
770
  dryRun: !!args["dry-run"],
727
- wsl: args.wsl || !!(args.wslDistro || args["wsl-distro"]),
728
- wslDistro: args.wslDistro || args["wsl-distro"] || "",
729
771
  npmRegistry: args.npmRegistry || "",
730
- pipIndexUrl: args.pipIndexUrl || ""
772
+ pipIndexUrl: args.pipIndexUrl || "",
773
+ yes: !!(args.yes || args.y),
774
+ noSetEnv: !!args["no-set-env"],
775
+ skipPluginInstall: !!(args["skip-plugin-install"] || args.skipNpmInstall),
776
+ skipCheck: !!args["skip-check"],
777
+ cmd: args.cmd || "",
778
+ configOverrides: {
779
+ baseUrl: args.langfuseBaseUrl || args.langfuseHost || args.host || "",
780
+ publicKey: args.publicKey || "",
781
+ secretKey: args.secretKey || "",
782
+ userId: args.userId || args.userid || ""
783
+ }
731
784
  };
732
785
  const [cmd, target] = args._;
733
786
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -2,8 +2,21 @@ import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
- import { execFileSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
5
+ import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
9
+ const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
10
+
11
+ function normalizeUserId(v) {
12
+ return String(v || "").trim();
13
+ }
14
+
15
+ function assertValidUserId(userId) {
16
+ if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
17
+ throw new Error(`工号格式不正确:--userId 必须匹配 ${USER_ID_PATTERN_TEXT},例如 h00613222 或 hwx1234567`);
18
+ }
19
+ }
7
20
 
8
21
  function parseArgs(argv) {
9
22
  const args = {};
@@ -136,10 +149,11 @@ async function main() {
136
149
  process.env.LANGFUSE_BASEURL ||
137
150
  process.env.LANGFUSE_HOST ||
138
151
  "http://120.46.221.227:3000";
139
- const userId = args.userId || args.userid || "";
152
+ const userId = normalizeUserId(args.userId || args.userid || "");
140
153
  if (!userId || typeof userId !== "string") {
141
154
  throw new Error("缺少参数:--userId=你的工号");
142
155
  }
156
+ assertValidUserId(userId);
143
157
  const pipIndexUrl = args.pipIndexUrl || process.env.LANGFUSE_PIP_INDEX_URL || "https://pypi.tuna.tsinghua.edu.cn/simple";
144
158
 
145
159
  if (!publicKey || !secretKey) {
@@ -2,7 +2,20 @@ import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
- import { execFileSync } from "node:child_process";
5
+ import { execFileSync } from "node:child_process";
6
+
7
+ const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
8
+ const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
9
+
10
+ function normalizeUserId(v) {
11
+ return String(v || "").trim();
12
+ }
13
+
14
+ function assertValidUserId(userId) {
15
+ if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
16
+ throw new Error(`工号格式不正确:--userId 必须匹配 ${USER_ID_PATTERN_TEXT},例如 h00613222 或 hwx1234567`);
17
+ }
18
+ }
6
19
 
7
20
  function parseArgs(argv) {
8
21
  const args = {};
@@ -158,10 +171,11 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
158
171
  async function main() {
159
172
  const args = parseArgs(process.argv.slice(2));
160
173
 
161
- const userId = args.userId || args.userid;
174
+ const userId = normalizeUserId(args.userId || args.userid);
162
175
  if (!userId || typeof userId !== "string") {
163
176
  throw new Error("缺少参数:--userId=你的工号");
164
177
  }
178
+ assertValidUserId(userId);
165
179
 
166
180
  const langfuseHost =
167
181
  args.langfuseHost ||
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { parseJsonRelaxed, stripBom } from "./json-utils.mjs";
5
- import { runOhLangfuseInWsl, shouldRunInWsl } from "./wsl-utils.mjs";
6
5
 
7
6
  function parseArgs(argv) {
8
7
  const args = {};
@@ -198,10 +197,7 @@ function main() {
198
197
  }
199
198
 
200
199
  try {
201
- const args = parseArgs(process.argv.slice(2));
202
- if (shouldRunInWsl(args)) {
203
- process.exit(runOhLangfuseInWsl(args, ["npx", "-y", "oh-langfuse@latest", "check", "opencode"]));
204
- }
200
+ parseArgs(process.argv.slice(2));
205
201
  main();
206
202
  } catch (e) {
207
203
  console.error(e?.message || String(e));
@@ -1,7 +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";
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
+ }
5
18
 
6
19
  function parseArgs(argv) {
7
20
  const args = {};
@@ -30,10 +43,11 @@ function main() {
30
43
  const secretKey =
31
44
  args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
32
45
  const baseUrl = args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || "http://120.46.221.227:3000";
33
- const userId = args.userId || args.userid || "";
46
+ const userId = normalizeUserId(args.userId || args.userid || "");
34
47
  if (!userId) {
35
48
  throw new Error("Missing userId. Run with --userId=your-id.");
36
49
  }
50
+ assertValidUserId(userId);
37
51
 
38
52
  // 1) 先执行 setup:默认会写入 Windows 用户级 LANGFUSE_*(从桌面/开始菜单启动 OpenCode 也能读到)
39
53
  const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
@@ -3,9 +3,21 @@ import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { spawn, spawnSync } from "node:child_process";
5
5
  import { parseJsonRelaxed, stripBom } from "./json-utils.mjs";
6
- import { runOhLangfuseInWsl, shouldRunInWsl } from "./wsl-utils.mjs";
7
-
8
- function parseArgs(argv) {
6
+
7
+ const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
8
+ const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
9
+
10
+ function normalizeUserId(v) {
11
+ return String(v || "").trim();
12
+ }
13
+
14
+ function assertValidUserId(userId) {
15
+ if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
16
+ throw new Error(`工号格式不正确:--userId 必须匹配 ${USER_ID_PATTERN_TEXT},例如 h00613222 或 hwx1234567`);
17
+ }
18
+ }
19
+
20
+ function parseArgs(argv) {
9
21
  const args = {};
10
22
  for (const raw of argv) {
11
23
  if (!raw.startsWith("--")) continue;
@@ -438,23 +450,6 @@ function updateShellConfig({ publicKey, secretKey, baseUrl, userId }) {
438
450
 
439
451
  async function main() {
440
452
  const args = parseArgs(process.argv.slice(2));
441
- if (shouldRunInWsl(args)) {
442
- const forwarded = [
443
- "npx",
444
- "-y",
445
- "oh-langfuse@latest",
446
- "setup",
447
- "opencode",
448
- ...(args.langfuseBaseUrl ? [`--langfuseBaseUrl=${args.langfuseBaseUrl}`] : []),
449
- ...(args.publicKey ? [`--publicKey=${args.publicKey}`] : []),
450
- ...(args.secretKey ? [`--secretKey=${args.secretKey}`] : []),
451
- ...(args.userId || args.userid ? [`--userId=${args.userId || args.userid}`] : []),
452
- ...(args.npmRegistry ? [`--npmRegistry=${args.npmRegistry}`] : []),
453
- ...(args["no-set-env"] ? ["--no-set-env"] : []),
454
- ...(args["skip-plugin-install"] ? ["--skip-plugin-install"] : [])
455
- ];
456
- process.exit(runOhLangfuseInWsl(args, forwarded));
457
- }
458
453
 
459
454
  const setEnv = !args["no-set-env"];
460
455
  const skipPluginInstall =
@@ -466,10 +461,11 @@ async function main() {
466
461
  args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
467
462
  const baseUrl =
468
463
  args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || "http://120.46.221.227:3000";
469
- const userId = args.userId || args.userid || "";
464
+ const userId = normalizeUserId(args.userId || args.userid || "");
470
465
  if (!userId || typeof userId !== "string") {
471
466
  throw new Error("缺少参数:--userId=你的工号");
472
467
  }
468
+ assertValidUserId(userId);
473
469
  const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || process.env.NPM_CONFIG_REGISTRY || "";
474
470
 
475
471
  const home = os.homedir();
@@ -546,7 +542,8 @@ async function main() {
546
542
  printCommandHint("如果新终端仍读不到环境变量,可运行:", unixLauncher);
547
543
  }
548
544
 
549
- if (setEnv) {
545
+ let shellConfigPathWritten = "";
546
+ if (setEnv) {
550
547
  if (process.platform === "win32") {
551
548
  console.log("写入用户级环境变量:LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASEURL …");
552
549
  setWindowsUserEnv({ publicKey, secretKey, baseUrl: baseUrl });
@@ -560,7 +557,8 @@ async function main() {
560
557
  console.log("提示:新开一个终端后环境变量才会对应用生效。");
561
558
  } else {
562
559
  console.log("正在写入环境变量到 shell 配置文件 …");
563
- const configPath = updateShellConfig({ publicKey, secretKey, baseUrl, userId });
560
+ const configPath = updateShellConfig({ publicKey, secretKey, baseUrl, userId });
561
+ shellConfigPathWritten = configPath;
564
562
  console.log(`已更新:${configPath}`);
565
563
 
566
564
  // 自动 source 配置文件
@@ -580,9 +578,21 @@ async function main() {
580
578
 
581
579
  console.log(paint("完成。", colors.green, colors.bold));
582
580
  printCommandHint("可运行以下命令校验:", "npx oh-langfuse@latest check opencode");
581
+ if (process.platform !== "win32") {
582
+ console.log("");
583
+ console.log("Important for WSL/Linux:");
584
+ console.log("This installer wrote LANGFUSE_* to your shell rc file, but it cannot update an already-open parent shell.");
585
+ if (shellConfigPathWritten) {
586
+ printCommandHint("Before starting OpenCode from this same terminal, run:", `source ${shellConfigPathWritten}`);
587
+ }
588
+ if (unixLauncher) {
589
+ printCommandHint("Or start OpenCode with the generated launcher:", unixLauncher);
590
+ }
591
+ console.log("Opening a new WSL terminal also loads the variables for normal shell setups.");
592
+ }
583
593
  }
584
-
585
- try {
594
+
595
+ try {
586
596
  await main();
587
597
  } catch (e) {
588
598
  console.error(e?.message || String(e));
@@ -1,39 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
-
3
- export function shouldRunInWsl(args) {
4
- return process.platform === "win32" && !!(args.wsl || args.wslDistro || args["wsl-distro"]);
5
- }
6
-
7
- function wslDistro(args) {
8
- if (typeof args.wsl === "string" && args.wsl.trim() && args.wsl.trim().toLowerCase() !== "true") {
9
- return args.wsl.trim();
10
- }
11
- if (typeof args.wslDistro === "string" && args.wslDistro.trim()) return args.wslDistro.trim();
12
- if (typeof args["wsl-distro"] === "string" && args["wsl-distro"].trim()) return args["wsl-distro"].trim();
13
- return "";
14
- }
15
-
16
- function shQuote(value) {
17
- return `'${String(value).replace(/'/g, "'\"'\"'")}'`;
18
- }
19
-
20
- export function runOhLangfuseInWsl(args, ohLangfuseArgs) {
21
- const distro = wslDistro(args);
22
- const wslArgs = [];
23
- if (distro) wslArgs.push("-d", distro);
24
- wslArgs.push("--", "sh", "-lc", ohLangfuseArgs.map(shQuote).join(" "));
25
-
26
- console.log(
27
- distro
28
- ? `Forwarding OpenCode Langfuse command to WSL distro: ${distro}`
29
- : "Forwarding OpenCode Langfuse command to the default WSL distro"
30
- );
31
-
32
- const r = spawnSync("wsl.exe", wslArgs, { stdio: "inherit", windowsHide: true });
33
- if (r.error) {
34
- console.error(r.error.message);
35
- console.error("WSL is not available from this Windows session. Run the same oh-langfuse command inside WSL instead.");
36
- return 1;
37
- }
38
- return r.status ?? 1;
39
- }