oh-aicoding-tool 0.1.2 → 0.1.4
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 +79 -80
- package/bin/cli.js +257 -384
- package/package.json +28 -56
- package/CODEX_LANGFUSE_PLAN.md +0 -62
- package/bin/langfuse-cli.js +0 -718
- package/codex_langfuse_notify.py +0 -591
- package/langfuse_hook.py +0 -603
- package/opencode-ohai-report/.claude/commands/report-ai-issue.md +0 -60
- package/opencode-ohai-report/.opencode/commands/report-ai-issue.md +0 -30
- package/opencode-ohai-report/.opencode/plugins/oh-ai-report.ts +0 -569
- package/opencode-ohai-report/README.md +0 -45
- package/opencode-ohai-report/bin/cli.js +0 -421
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-architecture.md +0 -313
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-best-practices.md +0 -476
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-phase1-summary.md +0 -405
- package/opencode-ohai-report/examples/issue_output.json +0 -4
- package/opencode-ohai-report/package.json +0 -40
- package/opencode-ohai-report/scripts/claude_report_hook.py +0 -257
- package/opencode-ohai-report/scripts/create_issue.py +0 -34
- package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +0 -254
- package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +0 -264
- package/opencode-ohai-report/scripts/install-opencode-plugin.sh +0 -218
- package/opencode-ohai-report/scripts/merge-claude-settings.py +0 -99
- package/opencode-ohai-report/tools/ohai-report/README.md +0 -151
- package/opencode-ohai-report/tools/ohai-report/examples/issue-input.json +0 -26
- package/opencode-ohai-report/tools/ohai-report/ohai_report/__init__.py +0 -5
- package/opencode-ohai-report/tools/ohai-report/ohai_report/__main__.py +0 -9
- package/opencode-ohai-report/tools/ohai-report/ohai_report/cli.py +0 -319
- package/opencode-ohai-report/tools/ohai-report/ohai_report/git_context.py +0 -32
- package/opencode-ohai-report/tools/ohai-report/ohai_report/gitcode_defaults.py +0 -14
- package/opencode-ohai-report/tools/ohai-report/ohai_report/issue_markdown.py +0 -313
- package/opencode-ohai-report/tools/ohai-report/ohai_report/metadata.py +0 -360
- package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/__init__.py +0 -1
- package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/langfuse.py +0 -38
- package/opencode-ohai-report/tools/ohai-report/ohai_report/payload.py +0 -64
- package/opencode-ohai-report/tools/ohai-report/ohai_report/schema.py +0 -80
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/__init__.py +0 -1
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/base.py +0 -15
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/gitcode.py +0 -405
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/local.py +0 -21
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/webhook.py +0 -354
- package/opencode-ohai-report/tools/ohai-report/ohai_report/webhook_defaults.py +0 -9
- package/opencode-ohai-report/tools/ohai-report/ohai_report/workspace.py +0 -61
- package/opencode-ohai-report/tools/ohai-report/ohai_report.py +0 -10
- package/opencode-ohai-report/tools/ohai-report/schemas/report_issue.schema.json +0 -166
- package/scripts/codex-langfuse-check.mjs +0 -101
- package/scripts/codex-langfuse-setup.mjs +0 -181
- package/scripts/langfuse-check.mjs +0 -90
- package/scripts/langfuse-setup.mjs +0 -278
- package/scripts/opencode-langfuse-check.mjs +0 -94
- package/scripts/opencode-langfuse-run.mjs +0 -96
- package/scripts/opencode-langfuse-setup.mjs +0 -478
- package/scripts/resolve-opencode-cli.mjs +0 -58
- package/setup-langfuse.bat +0 -163
- package/setup-langfuse.sh +0 -130
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
|
|
5
|
-
function stripBom(s) {
|
|
6
|
-
if (typeof s !== "string" || s.length === 0) return s;
|
|
7
|
-
return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function readJsonIfExists(p) {
|
|
11
|
-
if (!fs.existsSync(p)) return null;
|
|
12
|
-
const txt = stripBom(fs.readFileSync(p, "utf8"));
|
|
13
|
-
if (!txt.trim()) return null;
|
|
14
|
-
return JSON.parse(txt);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function hasPlugin(pluginField, name) {
|
|
18
|
-
if (Array.isArray(pluginField)) return pluginField.includes(name);
|
|
19
|
-
if (typeof pluginField === "string") return pluginField === name;
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function main() {
|
|
24
|
-
const home = os.homedir();
|
|
25
|
-
const opencodeDir = path.join(home, ".config", "opencode");
|
|
26
|
-
const pkgDir = path.join(opencodeDir, "node_modules", "opencode-plugin-langfuse");
|
|
27
|
-
const pluginDest = path.join(opencodeDir, "plugins", "opencode-plugin-langfuse");
|
|
28
|
-
const langfusePluginPath = "./plugins/opencode-plugin-langfuse";
|
|
29
|
-
const opencodeJsonPath = path.join(opencodeDir, "opencode.json");
|
|
30
|
-
|
|
31
|
-
const results = [];
|
|
32
|
-
|
|
33
|
-
results.push({ item: "OpenCode 配置目录", ok: fs.existsSync(opencodeDir), detail: opencodeDir });
|
|
34
|
-
|
|
35
|
-
const settings = readJsonIfExists(opencodeJsonPath);
|
|
36
|
-
results.push({
|
|
37
|
-
item: "opencode.json",
|
|
38
|
-
ok: !!settings,
|
|
39
|
-
detail: opencodeJsonPath
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const otOk = !!(settings?.experimental?.openTelemetry === true);
|
|
43
|
-
results.push({
|
|
44
|
-
item: "experimental.openTelemetry",
|
|
45
|
-
ok: otOk,
|
|
46
|
-
detail: otOk ? "true" : String(settings?.experimental?.openTelemetry ?? "缺失")
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const plOk = hasPlugin(settings?.plugin, langfusePluginPath);
|
|
50
|
-
results.push({
|
|
51
|
-
item: `plugin 包含 ${langfusePluginPath}`,
|
|
52
|
-
ok: plOk,
|
|
53
|
-
detail: JSON.stringify(settings?.plugin ?? null)
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
results.push({
|
|
57
|
-
item: "npm 包目录 node_modules/opencode-plugin-langfuse",
|
|
58
|
-
ok: fs.existsSync(pkgDir),
|
|
59
|
-
detail: pkgDir
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const launcherPath = path.join(opencodeDir, "launch-opencode-langfuse.cmd");
|
|
63
|
-
if (process.platform === "win32") {
|
|
64
|
-
results.push({
|
|
65
|
-
item: "plugins 目录(Windows 要求)",
|
|
66
|
-
ok: fs.existsSync(path.join(pluginDest, "package.json")),
|
|
67
|
-
detail: pluginDest
|
|
68
|
-
});
|
|
69
|
-
results.push({
|
|
70
|
-
item: "启动脚本 launch-opencode-langfuse.cmd",
|
|
71
|
-
ok: fs.existsSync(launcherPath),
|
|
72
|
-
detail: launcherPath
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const hasPk = !!process.env.LANGFUSE_PUBLIC_KEY;
|
|
77
|
-
results.push({
|
|
78
|
-
item: "当前进程 LANGFUSE_PUBLIC_KEY(任选终端验证)",
|
|
79
|
-
ok: hasPk,
|
|
80
|
-
detail: hasPk ? "已设置(本终端可见)" : "未设置:若你只从桌面打开 OpenCode,请先运行 npm run opencode:langfuse:setup 或使用 launch-opencode-langfuse.cmd"
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const w = Math.max(...results.map((r) => r.item.length)) + 2;
|
|
84
|
-
for (const r of results) {
|
|
85
|
-
const status = r.ok ? "OK " : "BAD";
|
|
86
|
-
const pad = (s, n) => (s.length >= n ? s : s + " ".repeat(n - s.length));
|
|
87
|
-
console.log(`${status} ${pad(r.item, w)} ${r.detail}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const failed = results.filter((r) => !r.ok);
|
|
91
|
-
process.exit(failed.length ? 2 : 0);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
main();
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { resolveOpencodeCli } from "./resolve-opencode-cli.mjs";
|
|
5
|
-
|
|
6
|
-
function parseArgs(argv) {
|
|
7
|
-
const args = {};
|
|
8
|
-
for (const raw of argv) {
|
|
9
|
-
if (!raw.startsWith("--")) continue;
|
|
10
|
-
const eq = raw.indexOf("=");
|
|
11
|
-
if (eq === -1) {
|
|
12
|
-
args[raw.slice(2)] = true;
|
|
13
|
-
} else {
|
|
14
|
-
args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return args;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function runNodeScript(scriptPath, extraArgs = []) {
|
|
21
|
-
const r = spawnSync(process.execPath, [scriptPath, ...extraArgs], { stdio: "inherit" });
|
|
22
|
-
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function main() {
|
|
26
|
-
const args = parseArgs(process.argv.slice(2));
|
|
27
|
-
|
|
28
|
-
const publicKey =
|
|
29
|
-
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
30
|
-
const secretKey =
|
|
31
|
-
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
32
|
-
const baseUrl = args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || "http://120.46.221.227:3000";
|
|
33
|
-
const userId = args.userId || args.userid || process.env.LANGFUSE_USER_ID || "";
|
|
34
|
-
|
|
35
|
-
// 1) 先执行 setup:默认会写入 Windows 用户级 LANGFUSE_*(从桌面/开始菜单启动 OpenCode 也能读到)
|
|
36
|
-
const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
-
const setupPath = path.join(scriptsDir, "opencode-langfuse-setup.mjs");
|
|
38
|
-
const setupExtra = [];
|
|
39
|
-
if (args["no-set-env"]) setupExtra.push("--no-set-env");
|
|
40
|
-
if (args["skip-plugin-install"]) setupExtra.push("--skip-plugin-install");
|
|
41
|
-
if (userId) setupExtra.push(`--userId=${userId}`);
|
|
42
|
-
runNodeScript(setupPath, setupExtra);
|
|
43
|
-
|
|
44
|
-
// 2) 直接带环境变量启动 opencode(本进程内有效)
|
|
45
|
-
const cmdArg = args.cmd || "opencode";
|
|
46
|
-
const cmdArgs = [];
|
|
47
|
-
if (typeof args.args === "string" && args.args.trim()) {
|
|
48
|
-
// 支持:--args="--foo bar"
|
|
49
|
-
cmdArgs.push(...args.args.split(" ").filter(Boolean));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const env = {
|
|
53
|
-
...process.env,
|
|
54
|
-
LANGFUSE_PUBLIC_KEY: publicKey,
|
|
55
|
-
LANGFUSE_SECRET_KEY: secretKey,
|
|
56
|
-
LANGFUSE_BASEURL: baseUrl,
|
|
57
|
-
...(userId ? { LANGFUSE_USER_ID: String(userId) } : {})
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const resolved = resolveOpencodeCli(cmdArg === "opencode" ? undefined : cmdArg);
|
|
61
|
-
if (!resolved) {
|
|
62
|
-
console.error(
|
|
63
|
-
[
|
|
64
|
-
"找不到 OpenCode CLI(不是桌面版)。请先安装命令行:",
|
|
65
|
-
" npm install -g opencode-ai@latest",
|
|
66
|
-
"安装后默认常见路径:%USERPROFILE%\\.opencode\\bin\\opencode.exe",
|
|
67
|
-
"若已安装仍找不到,可显式指定:npm run opencode:langfuse:run -- --cmd=C:\\\\完整路径\\\\opencode.exe"
|
|
68
|
-
].join("\n")
|
|
69
|
-
);
|
|
70
|
-
process.exit(127);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.log(`启动 OpenCode:${resolved} ${cmdArgs.join(" ")}`.trim() + ". userId: " + (userId || "<none>"));
|
|
74
|
-
const useShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(resolved);
|
|
75
|
-
const r = spawnSync(resolved, cmdArgs, { stdio: "inherit", env, shell: useShell });
|
|
76
|
-
if (r.error?.code === "ENOENT") {
|
|
77
|
-
console.error(
|
|
78
|
-
`无法执行 "${resolved}"。若这是 .cmd,请尝试:npm run opencode:langfuse:run -- --cmd=${path.join(
|
|
79
|
-
process.env.USERPROFILE || "",
|
|
80
|
-
".opencode",
|
|
81
|
-
"bin",
|
|
82
|
-
"opencode.exe"
|
|
83
|
-
)}`
|
|
84
|
-
);
|
|
85
|
-
process.exit(127);
|
|
86
|
-
}
|
|
87
|
-
process.exit(r.status ?? 0);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
main();
|
|
92
|
-
} catch (e) {
|
|
93
|
-
console.error(e?.message || String(e));
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
function parseArgs(argv) {
|
|
7
|
-
const args = {};
|
|
8
|
-
for (const raw of argv) {
|
|
9
|
-
if (!raw.startsWith("--")) continue;
|
|
10
|
-
const eq = raw.indexOf("=");
|
|
11
|
-
if (eq === -1) {
|
|
12
|
-
args[raw.slice(2)] = true;
|
|
13
|
-
} else {
|
|
14
|
-
args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return args;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function ensureDir(p) {
|
|
21
|
-
fs.mkdirSync(p, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function stripBom(s) {
|
|
25
|
-
if (typeof s !== "string" || s.length === 0) return s;
|
|
26
|
-
// Handle UTF-8 BOM (U+FEFF) which breaks JSON.parse on some files.
|
|
27
|
-
return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function readJsonIfExists(p) {
|
|
31
|
-
if (!fs.existsSync(p)) return null;
|
|
32
|
-
const txt = stripBom(fs.readFileSync(p, "utf8"));
|
|
33
|
-
if (!txt.trim()) return null;
|
|
34
|
-
return JSON.parse(txt);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function writeJsonPretty(p, obj) {
|
|
38
|
-
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + os.EOL, "utf8");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isObject(x) {
|
|
42
|
-
return x && typeof x === "object" && !Array.isArray(x);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function deepMerge(target, src) {
|
|
46
|
-
if (!isObject(target) || !isObject(src)) return src;
|
|
47
|
-
const out = { ...target };
|
|
48
|
-
for (const [k, v] of Object.entries(src)) {
|
|
49
|
-
if (isObject(v) && isObject(out[k])) out[k] = deepMerge(out[k], v);
|
|
50
|
-
else out[k] = v;
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function mergePluginList(existing, name) {
|
|
56
|
-
let arr = [];
|
|
57
|
-
if (Array.isArray(existing)) arr = [...existing];
|
|
58
|
-
else if (typeof existing === "string" && existing.trim()) arr = [existing];
|
|
59
|
-
if (!arr.includes(name)) arr.push(name);
|
|
60
|
-
return arr;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function removePlugins(existing, names) {
|
|
64
|
-
const removeSet = new Set(names);
|
|
65
|
-
if (Array.isArray(existing)) return existing.filter((x) => !removeSet.has(x));
|
|
66
|
-
if (typeof existing === "string") return removeSet.has(existing) ? [] : [existing];
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function psQuote(s) {
|
|
71
|
-
return `'${String(s).replace(/'/g, "''")}'`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function writeLangfusePluginUserConfig({ userId }) {
|
|
75
|
-
const home = os.homedir();
|
|
76
|
-
const dir = path.join(home, ".config", "opencode-plugin-langfuse");
|
|
77
|
-
const p = path.join(dir, "config.json");
|
|
78
|
-
ensureDir(dir);
|
|
79
|
-
const obj = { usrid: userId, userId };
|
|
80
|
-
writeJsonPretty(p, obj);
|
|
81
|
-
return p;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getPatchedLangfuseDistIndexJs() {
|
|
85
|
-
// This replaces opencode-plugin-langfuse/dist/index.js to inject userId into spans.
|
|
86
|
-
// It is intentionally self-contained (no extra build steps).
|
|
87
|
-
return [
|
|
88
|
-
'import { LangfuseSpanProcessor } from "@langfuse/otel";',
|
|
89
|
-
'import { promises as fs } from "node:fs";',
|
|
90
|
-
'import os from "node:os";',
|
|
91
|
-
'import path from "node:path";',
|
|
92
|
-
'import { NodeSDK } from "@opentelemetry/sdk-node";',
|
|
93
|
-
"",
|
|
94
|
-
"const USER_CONFIG_PATH = path.join(",
|
|
95
|
-
" os.homedir(),",
|
|
96
|
-
' ".config",',
|
|
97
|
-
' "opencode-plugin-langfuse",',
|
|
98
|
-
' "config.json"',
|
|
99
|
-
");",
|
|
100
|
-
"",
|
|
101
|
-
"const readConfiguredUserId = async () => {",
|
|
102
|
-
" try {",
|
|
103
|
-
' const content = await fs.readFile(USER_CONFIG_PATH, "utf8");',
|
|
104
|
-
" // tolerate UTF-8 BOM",
|
|
105
|
-
" const txt = content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;",
|
|
106
|
-
" const parsed = JSON.parse(txt);",
|
|
107
|
-
" const userId =",
|
|
108
|
-
' typeof parsed.userId === "string"',
|
|
109
|
-
" ? parsed.userId.trim()",
|
|
110
|
-
' : typeof parsed.usrid === "string"',
|
|
111
|
-
" ? parsed.usrid.trim()",
|
|
112
|
-
" : \"\";",
|
|
113
|
-
" return userId || undefined;",
|
|
114
|
-
" } catch {",
|
|
115
|
-
" return undefined;",
|
|
116
|
-
" }",
|
|
117
|
-
"};",
|
|
118
|
-
"",
|
|
119
|
-
"const createUserIdSpanProcessor = (userId) => ({",
|
|
120
|
-
" onStart: (span) => {",
|
|
121
|
-
' span.setAttribute("langfuse.user.id", userId);',
|
|
122
|
-
' span.setAttribute("user.id", userId);',
|
|
123
|
-
' span.setAttribute("ai.telemetry.metadata.userId", userId);',
|
|
124
|
-
' span.setAttribute("langfuse.trace.metadata.langfuse_user_id", userId);',
|
|
125
|
-
' span.setAttribute("langfuse.observation.metadata.langfuse_user_id", userId);',
|
|
126
|
-
' span.setAttribute("langfuse.metadata.langfuse_user_id", userId);',
|
|
127
|
-
" },",
|
|
128
|
-
" onEnd: (span) => {",
|
|
129
|
-
' span.setAttribute("langfuse.user.id", userId);',
|
|
130
|
-
' span.setAttribute("user.id", userId);',
|
|
131
|
-
' span.setAttribute("ai.telemetry.metadata.userId", userId);',
|
|
132
|
-
' span.setAttribute("langfuse.trace.metadata.langfuse_user_id", userId);',
|
|
133
|
-
' span.setAttribute("langfuse.observation.metadata.langfuse_user_id", userId);',
|
|
134
|
-
' span.setAttribute("langfuse.metadata.langfuse_user_id", userId);',
|
|
135
|
-
" },",
|
|
136
|
-
" forceFlush: async () => {},",
|
|
137
|
-
" shutdown: async () => {},",
|
|
138
|
-
"});",
|
|
139
|
-
"",
|
|
140
|
-
"export const LangfusePlugin = async ({ client }) => {",
|
|
141
|
-
" const publicKey = process.env.LANGFUSE_PUBLIC_KEY;",
|
|
142
|
-
" const secretKey = process.env.LANGFUSE_SECRET_KEY;",
|
|
143
|
-
' const baseUrl = process.env.LANGFUSE_BASEURL ?? "https://cloud.langfuse.com";',
|
|
144
|
-
' const environment = process.env.LANGFUSE_ENVIRONMENT ?? "development";',
|
|
145
|
-
"",
|
|
146
|
-
" const userIdFromConfig = await readConfiguredUserId();",
|
|
147
|
-
" const userIdEnv = process.env.LANGFUSE_USER_ID?.trim();",
|
|
148
|
-
" const userId = userIdFromConfig || userIdEnv;",
|
|
149
|
-
"",
|
|
150
|
-
" const log = (level, message) => {",
|
|
151
|
-
' client.app.log({ body: { service: "langfuse-otel", level, message } });',
|
|
152
|
-
" };",
|
|
153
|
-
"",
|
|
154
|
-
" if (!publicKey || !secretKey) {",
|
|
155
|
-
' log("warn", "Missing LANGFUSE_PUBLIC_KEY or LANGFUSE_SECRET_KEY - tracing disabled");',
|
|
156
|
-
" return {};",
|
|
157
|
-
" }",
|
|
158
|
-
"",
|
|
159
|
-
" const processor = new LangfuseSpanProcessor({ publicKey, secretKey, baseUrl, environment });",
|
|
160
|
-
" const spanProcessors = [processor];",
|
|
161
|
-
" if (userId) spanProcessors.push(createUserIdSpanProcessor(userId));",
|
|
162
|
-
"",
|
|
163
|
-
" const sdk = new NodeSDK({ spanProcessors });",
|
|
164
|
-
" sdk.start();",
|
|
165
|
-
"",
|
|
166
|
-
' log("info", `OTEL tracing initialized -> ${baseUrl}`);',
|
|
167
|
-
' if (userId) log("info", `LANGFUSE userId configured -> ${userId}`);',
|
|
168
|
-
"",
|
|
169
|
-
" return {",
|
|
170
|
-
" config: async (config) => {",
|
|
171
|
-
" if (!config.experimental?.openTelemetry) {",
|
|
172
|
-
' log("warn", "OpenTelemetry experimental feature is disabled in Opencode config - tracing disabled");',
|
|
173
|
-
" }",
|
|
174
|
-
" },",
|
|
175
|
-
" event: async ({ event }) => {",
|
|
176
|
-
' if (event.type === "session.idle") {',
|
|
177
|
-
' log("info", "Flushing OTEL spans before idle");',
|
|
178
|
-
" await processor.forceFlush();",
|
|
179
|
-
" }",
|
|
180
|
-
' if (event.type === "server.instance.disposed") {',
|
|
181
|
-
' log("info", "Shutting down OTEL SDK before dispose");',
|
|
182
|
-
" await sdk.shutdown();",
|
|
183
|
-
" }",
|
|
184
|
-
" },",
|
|
185
|
-
" };",
|
|
186
|
-
"};",
|
|
187
|
-
""
|
|
188
|
-
].join(os.EOL);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function patchDistIndexJs(distIndexPath) {
|
|
192
|
-
const code = getPatchedLangfuseDistIndexJs();
|
|
193
|
-
ensureDir(path.dirname(distIndexPath));
|
|
194
|
-
fs.writeFileSync(distIndexPath, code + os.EOL, "utf8");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function writeWindowsLauncherCmd(opencodeDir, { publicKey, secretKey, baseUrl, userId }) {
|
|
198
|
-
if (process.platform !== "win32") return null;
|
|
199
|
-
const p = path.join(opencodeDir, "launch-opencode-langfuse.cmd");
|
|
200
|
-
const cmd = ["@echo off", "REM Auto-generated by scripts/opencode-langfuse-setup.mjs"];
|
|
201
|
-
cmd.push(`set LANGFUSE_PUBLIC_KEY=${publicKey}`);
|
|
202
|
-
cmd.push(`set LANGFUSE_SECRET_KEY=${secretKey}`);
|
|
203
|
-
cmd.push(`set LANGFUSE_BASEURL=${baseUrl}`);
|
|
204
|
-
if (userId) cmd.push(`set LANGFUSE_USER_ID=${userId}`);
|
|
205
|
-
cmd.push('if exist "%USERPROFILE%\\.opencode\\bin\\opencode.exe" (');
|
|
206
|
-
cmd.push(' "%USERPROFILE%\\.opencode\\bin\\opencode.exe" %*');
|
|
207
|
-
cmd.push(" exit /b %ERRORLEVEL%");
|
|
208
|
-
cmd.push(")");
|
|
209
|
-
cmd.push("opencode %*");
|
|
210
|
-
fs.writeFileSync(p, cmd.join("\r\n") + "\r\n", "utf8");
|
|
211
|
-
return p;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function getNpmExecutable() {
|
|
215
|
-
if (process.platform === "win32") {
|
|
216
|
-
const beside = path.join(path.dirname(process.execPath), "npm.cmd");
|
|
217
|
-
if (fs.existsSync(beside)) return beside;
|
|
218
|
-
}
|
|
219
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** 与 node.exe 同目录下的 npm-cli.js(避免 Windows 上直接 spawn `*.cmd` 出现 EINVAL)。 */
|
|
223
|
-
function getNpmCliJsPath() {
|
|
224
|
-
return path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** @returns {{ status: number | null, stdout: string, stderr: string, error?: Error }} */
|
|
228
|
-
function runNpmCapture(npmArgs) {
|
|
229
|
-
const cliJs = getNpmCliJsPath();
|
|
230
|
-
if (fs.existsSync(cliJs)) {
|
|
231
|
-
return spawnSync(process.execPath, [cliJs, ...npmArgs], {
|
|
232
|
-
encoding: "utf8",
|
|
233
|
-
windowsHide: true,
|
|
234
|
-
maxBuffer: 10 * 1024 * 1024
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
const npmExe = getNpmExecutable();
|
|
238
|
-
const useShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(npmExe);
|
|
239
|
-
return spawnSync(npmExe, npmArgs, {
|
|
240
|
-
encoding: "utf8",
|
|
241
|
-
shell: useShell,
|
|
242
|
-
windowsHide: true,
|
|
243
|
-
maxBuffer: 10 * 1024 * 1024
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function printNpmDiagnostics() {
|
|
248
|
-
try {
|
|
249
|
-
const nodeV = spawnSync(process.execPath, ["-v"], { encoding: "utf8" });
|
|
250
|
-
console.error("诊断:node 版本 =", (nodeV.stdout || "").trim(), "exit =", nodeV.status);
|
|
251
|
-
} catch (_) {}
|
|
252
|
-
const cliJs = getNpmCliJsPath();
|
|
253
|
-
console.error("诊断:npm 入口 =", fs.existsSync(cliJs) ? `node + ${cliJs}` : getNpmExecutable());
|
|
254
|
-
try {
|
|
255
|
-
const npmVersion = runNpmCapture(["-v"]);
|
|
256
|
-
console.error(
|
|
257
|
-
"诊断:npm 版本 =",
|
|
258
|
-
(npmVersion.stdout || "").trim(),
|
|
259
|
-
"exit =",
|
|
260
|
-
npmVersion.status,
|
|
261
|
-
npmVersion.error || ""
|
|
262
|
-
);
|
|
263
|
-
} catch (e) {
|
|
264
|
-
console.error("诊断:无法执行 npm -v:", e?.message || e);
|
|
265
|
-
}
|
|
266
|
-
try {
|
|
267
|
-
const reg = runNpmCapture(["config", "get", "registry"]);
|
|
268
|
-
console.error("诊断:npm registry =", (reg.stdout || "").trim());
|
|
269
|
-
if (reg.stderr) console.error(reg.stderr);
|
|
270
|
-
} catch (_) {}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-langfuse" }) {
|
|
274
|
-
const npmArgs = ["install", pkgName, "--prefix", opencodeDir];
|
|
275
|
-
const cliJs = getNpmCliJsPath();
|
|
276
|
-
console.log(`使用 npm:${fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable()}`);
|
|
277
|
-
const r = runNpmCapture(npmArgs);
|
|
278
|
-
if (r.stdout) process.stdout.write(r.stdout);
|
|
279
|
-
if (r.stderr) process.stderr.write(r.stderr);
|
|
280
|
-
if (!r.error && r.status === 0) return;
|
|
281
|
-
printNpmDiagnostics();
|
|
282
|
-
const npmLabel = fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable();
|
|
283
|
-
throw new Error(
|
|
284
|
-
`执行 ${npmLabel} ${npmArgs.join(" ")} 失败(exit=${r.status}${r.error ? ` ${r.error.message}` : ""})。` +
|
|
285
|
-
`见 README「常见问题」或设置 OPENCODE_SKIP_PLUGIN_INSTALL=1 后重试。`
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function setWindowsUserEnv({ publicKey, secretKey, baseUrl }) {
|
|
290
|
-
const cmd = [
|
|
291
|
-
"$ErrorActionPreference = 'Stop';",
|
|
292
|
-
`[Environment]::SetEnvironmentVariable('LANGFUSE_PUBLIC_KEY', ${psQuote(publicKey)}, 'User');`,
|
|
293
|
-
`[Environment]::SetEnvironmentVariable('LANGFUSE_SECRET_KEY', ${psQuote(secretKey)}, 'User');`,
|
|
294
|
-
`[Environment]::SetEnvironmentVariable('LANGFUSE_BASEURL', ${psQuote(baseUrl)}, 'User');`
|
|
295
|
-
].join(" ");
|
|
296
|
-
const r = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
297
|
-
if (r.status !== 0) {
|
|
298
|
-
throw new Error("写入用户级环境变量失败(PowerShell SetEnvironmentVariable)。");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function getShellConfigPath() {
|
|
303
|
-
const shell = process.env.SHELL || "/bin/bash";
|
|
304
|
-
if (shell.includes("zsh")) {
|
|
305
|
-
return path.join(os.homedir(), ".zshrc");
|
|
306
|
-
}
|
|
307
|
-
return path.join(os.homedir(), ".bashrc");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function updateShellConfig({ publicKey, secretKey, baseUrl, userId }) {
|
|
311
|
-
const configPath = getShellConfigPath();
|
|
312
|
-
const marker = "# === Langfuse OpenCode Setup ===";
|
|
313
|
-
const endMarker = "# === End Langfuse OpenCode Setup ===";
|
|
314
|
-
|
|
315
|
-
let content = "";
|
|
316
|
-
if (fs.existsSync(configPath)) {
|
|
317
|
-
content = fs.readFileSync(configPath, "utf8");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 移除旧的配置块
|
|
321
|
-
const startIdx = content.indexOf(marker);
|
|
322
|
-
if (startIdx !== -1) {
|
|
323
|
-
const endIdx = content.indexOf(endMarker, startIdx);
|
|
324
|
-
if (endIdx !== -1) {
|
|
325
|
-
content = content.slice(0, startIdx) + content.slice(endIdx + endMarker.length);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 构建新的配置块
|
|
330
|
-
const envBlock = [
|
|
331
|
-
"",
|
|
332
|
-
marker,
|
|
333
|
-
`export LANGFUSE_PUBLIC_KEY="${publicKey}"`,
|
|
334
|
-
`export LANGFUSE_SECRET_KEY="${secretKey}"`,
|
|
335
|
-
`export LANGFUSE_BASEURL="${baseUrl}"`,
|
|
336
|
-
userId ? `export LANGFUSE_USER_ID="${userId}"` : null,
|
|
337
|
-
endMarker,
|
|
338
|
-
""
|
|
339
|
-
].filter(Boolean).join("\n");
|
|
340
|
-
|
|
341
|
-
// 追加到文件末尾
|
|
342
|
-
content = content.trimEnd() + "\n" + envBlock + "\n";
|
|
343
|
-
fs.writeFileSync(configPath, content, "utf8");
|
|
344
|
-
|
|
345
|
-
return configPath;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function main() {
|
|
349
|
-
const args = parseArgs(process.argv.slice(2));
|
|
350
|
-
const setEnv = !args["no-set-env"];
|
|
351
|
-
const skipPluginInstall =
|
|
352
|
-
!!(args["skip-plugin-install"] || args.skipNpmInstall || process.env.OPENCODE_SKIP_PLUGIN_INSTALL);
|
|
353
|
-
|
|
354
|
-
const publicKey =
|
|
355
|
-
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
356
|
-
const secretKey =
|
|
357
|
-
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
358
|
-
const baseUrl =
|
|
359
|
-
args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || "http://120.46.221.227:3000";
|
|
360
|
-
const userId = args.userId || args.userid || process.env.LANGFUSE_USER_ID || process.env.OPENCODE_USER_ID || "";
|
|
361
|
-
|
|
362
|
-
const home = os.homedir();
|
|
363
|
-
const opencodeDir = path.join(home, ".config", "opencode");
|
|
364
|
-
const pluginsDir = path.join(opencodeDir, "plugins");
|
|
365
|
-
const pkgDir = path.join(opencodeDir, "node_modules", "opencode-plugin-langfuse");
|
|
366
|
-
const pluginDest = path.join(pluginsDir, "opencode-plugin-langfuse");
|
|
367
|
-
const opencodeJsonPath = path.join(opencodeDir, "opencode.json");
|
|
368
|
-
|
|
369
|
-
ensureDir(opencodeDir);
|
|
370
|
-
|
|
371
|
-
console.log(`OpenCode 配置目录:${opencodeDir}`);
|
|
372
|
-
|
|
373
|
-
if (skipPluginInstall && fs.existsSync(pkgDir)) {
|
|
374
|
-
console.log(`已跳过 npm install(--skip-plugin-install 且已存在):${pkgDir}`);
|
|
375
|
-
} else {
|
|
376
|
-
console.log("安装 npm 包:opencode-plugin-langfuse …");
|
|
377
|
-
runNpmInstallOrThrow({ opencodeDir });
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (!fs.existsSync(pkgDir)) {
|
|
381
|
-
throw new Error(
|
|
382
|
-
`未找到已安装包目录:${pkgDir}。请先解决上方 npm 报错,或手动在此目录执行 npm install opencode-plugin-langfuse。`
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (process.platform === "win32") {
|
|
387
|
-
console.log("Windows:将插件复制到 ~/.config/opencode/plugins/ …");
|
|
388
|
-
ensureDir(pluginsDir);
|
|
389
|
-
if (fs.existsSync(pluginDest)) {
|
|
390
|
-
fs.rmSync(pluginDest, { recursive: true, force: true });
|
|
391
|
-
}
|
|
392
|
-
fs.cpSync(pkgDir, pluginDest, { recursive: true });
|
|
393
|
-
console.log(`已复制到:${pluginDest}`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const existing = readJsonIfExists(opencodeJsonPath) ?? {};
|
|
397
|
-
// Patch the official plugin to inject userId and make behavior consistent.
|
|
398
|
-
const distIndexInNodeModules = path.join(pkgDir, "dist", "index.js");
|
|
399
|
-
if (fs.existsSync(distIndexInNodeModules)) {
|
|
400
|
-
patchDistIndexJs(distIndexInNodeModules);
|
|
401
|
-
}
|
|
402
|
-
if (process.platform === "win32") {
|
|
403
|
-
const distIndexInPlugins = path.join(pluginDest, "dist", "index.js");
|
|
404
|
-
if (fs.existsSync(distIndexInPlugins)) {
|
|
405
|
-
patchDistIndexJs(distIndexInPlugins);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Write plugin user config to support userId injection.
|
|
410
|
-
if (userId) {
|
|
411
|
-
const cfg = writeLangfusePluginUserConfig({ userId });
|
|
412
|
-
console.log(`已更新:${cfg}`);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const langfusePluginPath = "./plugins/opencode-plugin-langfuse";
|
|
416
|
-
const basePlugins = removePlugins(existing.plugin, [
|
|
417
|
-
"opencode-plugin-langfuse-userid",
|
|
418
|
-
"opencode-plugin-langfuse",
|
|
419
|
-
langfusePluginPath
|
|
420
|
-
]);
|
|
421
|
-
const desired = {
|
|
422
|
-
experimental: {
|
|
423
|
-
openTelemetry: true
|
|
424
|
-
},
|
|
425
|
-
plugin: mergePluginList(basePlugins, langfusePluginPath)
|
|
426
|
-
};
|
|
427
|
-
const merged = deepMerge(existing, desired);
|
|
428
|
-
writeJsonPretty(opencodeJsonPath, merged);
|
|
429
|
-
console.log(`已更新:${opencodeJsonPath}`);
|
|
430
|
-
|
|
431
|
-
const launcher = writeWindowsLauncherCmd(opencodeDir, { publicKey, secretKey, baseUrl, userId });
|
|
432
|
-
if (launcher) {
|
|
433
|
-
console.log(`已生成快捷启动脚本(含 LANGFUSE 环境变量):${launcher}`);
|
|
434
|
-
console.log("若不从当前终端启动,也可双击或用该脚本启动 OpenCode(避免插件读不到密钥)。");
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (setEnv) {
|
|
438
|
-
if (process.platform === "win32") {
|
|
439
|
-
console.log("写入用户级环境变量:LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASEURL …");
|
|
440
|
-
setWindowsUserEnv({ publicKey, secretKey, baseUrl: baseUrl });
|
|
441
|
-
if (userId) {
|
|
442
|
-
const cmd = [
|
|
443
|
-
"$ErrorActionPreference = 'Stop';",
|
|
444
|
-
`[Environment]::SetEnvironmentVariable('LANGFUSE_USER_ID', ${psQuote(userId)}, 'User');`
|
|
445
|
-
].join(" ");
|
|
446
|
-
spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
447
|
-
}
|
|
448
|
-
console.log("提示:新开一个终端后环境变量才会对应用生效。");
|
|
449
|
-
} else {
|
|
450
|
-
console.log("正在写入环境变量到 shell 配置文件 …");
|
|
451
|
-
const configPath = updateShellConfig({ publicKey, secretKey, baseUrl, userId });
|
|
452
|
-
console.log(`已更新:${configPath}`);
|
|
453
|
-
|
|
454
|
-
// 自动 source 配置文件
|
|
455
|
-
console.log("正在 source 配置文件使环境变量生效 …");
|
|
456
|
-
const r = spawnSync("bash", ["-c", `source "${configPath}" && env | grep -E '^LANGFUSE_'`], {
|
|
457
|
-
encoding: "utf8",
|
|
458
|
-
stdio: "pipe"
|
|
459
|
-
});
|
|
460
|
-
if (r.status === 0) {
|
|
461
|
-
console.log("环境变量已生效:");
|
|
462
|
-
console.log(r.stdout);
|
|
463
|
-
} else {
|
|
464
|
-
console.log("提示:环境变量已写入配置文件,请运行以下命令使其生效:");
|
|
465
|
-
console.log(`source ${configPath}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
console.log("完成。可运行:`npm run opencode:langfuse:check` 校验。");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
try {
|
|
474
|
-
main();
|
|
475
|
-
} catch (e) {
|
|
476
|
-
console.error(e?.message || String(e));
|
|
477
|
-
process.exit(1);
|
|
478
|
-
}
|