oh-langfuse 0.1.17 → 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 +1 -1
- package/bin/cli.js +53 -23
- package/package.json +1 -1
- package/scripts/codex-langfuse-setup.mjs +17 -3
- package/scripts/langfuse-setup.mjs +16 -2
- package/scripts/opencode-langfuse-run.mjs +17 -3
- package/scripts/opencode-langfuse-setup.mjs +17 -3
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
|
|
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();
|
|
@@ -456,7 +478,7 @@ function langfuseConfig(overrides = {}) {
|
|
|
456
478
|
baseUrl: envDefault("LANGFUSE_BASEURL", envDefault("LANGFUSE_HOST", DEFAULT_LANGFUSE_BASE_URL)),
|
|
457
479
|
publicKey: envDefault("LANGFUSE_PUBLIC_KEY", DEFAULT_LANGFUSE_PUBLIC_KEY),
|
|
458
480
|
secretKey: envDefault("LANGFUSE_SECRET_KEY", DEFAULT_LANGFUSE_SECRET_KEY),
|
|
459
|
-
userId:
|
|
481
|
+
userId: ""
|
|
460
482
|
};
|
|
461
483
|
for (const [key, value] of Object.entries(overrides)) {
|
|
462
484
|
if (hasValue(value)) config[key] = value;
|
|
@@ -471,10 +493,18 @@ async function collectLangfuseConfig(rl, { requireUserId = false, overrides = {}
|
|
|
471
493
|
labelValue("Public Key", config.publicKey, t.blue),
|
|
472
494
|
labelValue("Secret Key", "configured", t.teal)
|
|
473
495
|
]);
|
|
474
|
-
if (
|
|
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
|
+
}
|
|
475
503
|
config.userId = await askText(rl, "User ID / employee number, for example h00613222", {
|
|
476
|
-
defaultValue:
|
|
477
|
-
required: requireUserId
|
|
504
|
+
defaultValue: "",
|
|
505
|
+
required: requireUserId,
|
|
506
|
+
validate: isValidUserId,
|
|
507
|
+
invalidMessage: userIdValidationMessage()
|
|
478
508
|
});
|
|
479
509
|
return config;
|
|
480
510
|
}
|
|
@@ -504,7 +534,7 @@ async function confirmAction(rl, title, rows, options) {
|
|
|
504
534
|
renderSection(title, rows);
|
|
505
535
|
if (options.dryRun || options.yes) return true;
|
|
506
536
|
console.log("");
|
|
507
|
-
return await askYesNo(rl, "Continue with these changes", { defaultValue:
|
|
537
|
+
return await askYesNo(rl, "Continue with these changes", { defaultValue: false });
|
|
508
538
|
}
|
|
509
539
|
|
|
510
540
|
async function setupClaude(rl, options) {
|
|
@@ -687,7 +717,7 @@ async function setupLangfuseMenu(rl, options) {
|
|
|
687
717
|
rl,
|
|
688
718
|
"Select Langfuse setup targets",
|
|
689
719
|
[
|
|
690
|
-
{ 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." },
|
|
691
721
|
{ label: "OpenCode Langfuse", value: "opencode", selected: false, description: "Install the Langfuse plugin and enable OpenTelemetry." },
|
|
692
722
|
{ label: "Codex Langfuse", value: "codex", selected: false, description: "Install the Codex notify hook and connect session JSONL events." }
|
|
693
723
|
],
|
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 ||
|
|
@@ -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,8 +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
|
-
|
|
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) {
|
|
8
21
|
const args = {};
|
|
9
22
|
for (const raw of argv) {
|
|
10
23
|
if (!raw.startsWith("--")) continue;
|
|
@@ -448,10 +461,11 @@ async function main() {
|
|
|
448
461
|
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
449
462
|
const baseUrl =
|
|
450
463
|
args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || "http://120.46.221.227:3000";
|
|
451
|
-
const userId = args.userId || args.userid || "";
|
|
464
|
+
const userId = normalizeUserId(args.userId || args.userid || "");
|
|
452
465
|
if (!userId || typeof userId !== "string") {
|
|
453
466
|
throw new Error("缺少参数:--userId=你的工号");
|
|
454
467
|
}
|
|
468
|
+
assertValidUserId(userId);
|
|
455
469
|
const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || process.env.NPM_CONFIG_REGISTRY || "";
|
|
456
470
|
|
|
457
471
|
const home = os.homedir();
|