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 +7 -8
- package/bin/cli.js +144 -91
- package/package.json +1 -1
- package/scripts/codex-langfuse-setup.mjs +17 -3
- package/scripts/langfuse-setup.mjs +16 -2
- package/scripts/opencode-langfuse-check.mjs +1 -5
- package/scripts/opencode-langfuse-run.mjs +17 -3
- package/scripts/opencode-langfuse-setup.mjs +35 -25
- package/scripts/wsl-utils.mjs +0 -39
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
|
|
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 (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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:
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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:
|
|
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
|
|
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:
|
|
537
|
+
return await askYesNo(rl, "Continue with these changes", { defaultValue: false });
|
|
510
538
|
}
|
|
511
539
|
|
|
512
|
-
async function setupClaude(rl, options) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
const
|
|
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",
|
|
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
|
-
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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",
|
|
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:
|
|
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("--
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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));
|
package/scripts/wsl-utils.mjs
DELETED
|
@@ -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
|
-
}
|