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.
Files changed (55) hide show
  1. package/README.md +79 -80
  2. package/bin/cli.js +257 -384
  3. package/package.json +28 -56
  4. package/CODEX_LANGFUSE_PLAN.md +0 -62
  5. package/bin/langfuse-cli.js +0 -718
  6. package/codex_langfuse_notify.py +0 -591
  7. package/langfuse_hook.py +0 -603
  8. package/opencode-ohai-report/.claude/commands/report-ai-issue.md +0 -60
  9. package/opencode-ohai-report/.opencode/commands/report-ai-issue.md +0 -30
  10. package/opencode-ohai-report/.opencode/plugins/oh-ai-report.ts +0 -569
  11. package/opencode-ohai-report/README.md +0 -45
  12. package/opencode-ohai-report/bin/cli.js +0 -421
  13. package/opencode-ohai-report/docs/opencode-ai-issue-collection-architecture.md +0 -313
  14. package/opencode-ohai-report/docs/opencode-ai-issue-collection-best-practices.md +0 -476
  15. package/opencode-ohai-report/docs/opencode-ai-issue-collection-phase1-summary.md +0 -405
  16. package/opencode-ohai-report/examples/issue_output.json +0 -4
  17. package/opencode-ohai-report/package.json +0 -40
  18. package/opencode-ohai-report/scripts/claude_report_hook.py +0 -257
  19. package/opencode-ohai-report/scripts/create_issue.py +0 -34
  20. package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +0 -254
  21. package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +0 -264
  22. package/opencode-ohai-report/scripts/install-opencode-plugin.sh +0 -218
  23. package/opencode-ohai-report/scripts/merge-claude-settings.py +0 -99
  24. package/opencode-ohai-report/tools/ohai-report/README.md +0 -151
  25. package/opencode-ohai-report/tools/ohai-report/examples/issue-input.json +0 -26
  26. package/opencode-ohai-report/tools/ohai-report/ohai_report/__init__.py +0 -5
  27. package/opencode-ohai-report/tools/ohai-report/ohai_report/__main__.py +0 -9
  28. package/opencode-ohai-report/tools/ohai-report/ohai_report/cli.py +0 -319
  29. package/opencode-ohai-report/tools/ohai-report/ohai_report/git_context.py +0 -32
  30. package/opencode-ohai-report/tools/ohai-report/ohai_report/gitcode_defaults.py +0 -14
  31. package/opencode-ohai-report/tools/ohai-report/ohai_report/issue_markdown.py +0 -313
  32. package/opencode-ohai-report/tools/ohai-report/ohai_report/metadata.py +0 -360
  33. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/__init__.py +0 -1
  34. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/langfuse.py +0 -38
  35. package/opencode-ohai-report/tools/ohai-report/ohai_report/payload.py +0 -64
  36. package/opencode-ohai-report/tools/ohai-report/ohai_report/schema.py +0 -80
  37. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/__init__.py +0 -1
  38. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/base.py +0 -15
  39. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/gitcode.py +0 -405
  40. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/local.py +0 -21
  41. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/webhook.py +0 -354
  42. package/opencode-ohai-report/tools/ohai-report/ohai_report/webhook_defaults.py +0 -9
  43. package/opencode-ohai-report/tools/ohai-report/ohai_report/workspace.py +0 -61
  44. package/opencode-ohai-report/tools/ohai-report/ohai_report.py +0 -10
  45. package/opencode-ohai-report/tools/ohai-report/schemas/report_issue.schema.json +0 -166
  46. package/scripts/codex-langfuse-check.mjs +0 -101
  47. package/scripts/codex-langfuse-setup.mjs +0 -181
  48. package/scripts/langfuse-check.mjs +0 -90
  49. package/scripts/langfuse-setup.mjs +0 -278
  50. package/scripts/opencode-langfuse-check.mjs +0 -94
  51. package/scripts/opencode-langfuse-run.mjs +0 -96
  52. package/scripts/opencode-langfuse-setup.mjs +0 -478
  53. package/scripts/resolve-opencode-cli.mjs +0 -58
  54. package/setup-langfuse.bat +0 -163
  55. package/setup-langfuse.sh +0 -130
@@ -1,30 +0,0 @@
1
- ---
2
- description: 上报 AI 辅助开发问题
3
- agent: build
4
- subtask: true
5
- ---
6
-
7
- 你是一个短事务问题上报 subagent。只做一件事:把用户描述整理成结构化字段,然后调用 OpenCode 插件工具 `report_ai_issue` 上报。不要继续参与当前开发任务。
8
-
9
- 硬性规则:
10
- - 必须调用 `report_ai_issue` 工具;工具不可用时,只回复:`report_ai_issue tool unavailable / 请重新运行 scripts/install-opencode-plugin.ps1 后完全重启 OpenCode`。
11
- - 禁止调用 Python、CLI、shell、`tools/ohai-report/ohai_report.py`、`scripts/create_issue.py` 或任何 fallback 命令。
12
- - 禁止根据本地生成的 `issue_id` 自行判断成功。
13
- - 只有工具返回的最后一行是可解析 JSON,且 JSON 中 `ok` 不是 `false`,并且存在非空 `webhook_url` 或 `gitcode_url`,才算上报成功。
14
- - 如果 JSON 中 `ok` 为 `false`,或没有 `webhook_url` / `gitcode_url`,只回复:`上报失败:<issue_id或unknown> <report_error或remote_url_missing>`。
15
- - 成功时只回复一行:`已上报:<issue_id> <webhook_url或gitcode_url>`。
16
- - 不要输出 Summary、Issue successfully reported、完整 Issue 正文、调试过程或额外解释。
17
-
18
- 必填字段:
19
- - `title`:标题,≤80 字。
20
- - `summary`:问题摘要,≤800 字。
21
- - `user_description`:用户原始描述。
22
- - `expected_behavior`:期望行为,≤400 字。
23
- - `actual_behavior`:实际行为,≤400 字。
24
- - `category`:只能是 `模型输出错误` / `工具调用失败` / `Skill 缺陷` / `上下文缺失` / `环境问题` / `其他`。
25
- - `severity`:`P0` / `P1` / `P2` / `P3`,默认 `P2`。
26
-
27
- 可选字段通过工具参数补充,不确定就省略或填 `unknown`:`scenario`、`task_type`、`workflow_phase`、`affected_role`、`primary_category`、`sub_category`、`classification_confidence`、`classification_rationale`、`impact`、`possible_causes`、`improvement_direction`、`suggested_follow_up`、`suggested_dispatch`。
28
-
29
- 用户描述:
30
- $ARGUMENTS
@@ -1,569 +0,0 @@
1
- import { readFileSync, existsSync } from "node:fs"
2
- import { homedir } from "node:os"
3
- import { dirname, join, normalize, parse, resolve } from "node:path"
4
- import { type Plugin, tool } from "@opencode-ai/plugin"
5
-
6
- /**
7
- * 工号配置(任选其一,优先级从上到下):
8
- * 1. 项目根 `opencode.json` → `plugin_config` 中指向本插件的条目(字段名见 `pickEmployeeIdFromPluginConfig`)
9
- * 2. 宿主环境变量 `OHAI_EMPLOYEE_ID` / `OPENCODE_EMPLOYEE_ID`
10
- *
11
- * OpenCode 在「安装/加载」本地插件时**不会**弹出交互式输入框;工号需在配置或环境中提供。
12
- *
13
- * `plugin_config` 示例(本地插件的 key 一般为 `plugin` 数组里写的同一字符串,如路径 `./.opencode/plugins/oh-ai-report.ts`):
14
- * ```json
15
- * {
16
- * "plugin": ["./.opencode/plugins/oh-ai-report.ts"],
17
- * "plugin_config": {
18
- * "./.opencode/plugins/oh-ai-report.ts": { "employeeId": "E12345" }
19
- * }
20
- * }
21
- * ```
22
- *
23
- * `ohai-report` CLI 路径:默认 `tools/ohai-report/ohai_report.py`(相对当前 OpenCode 工作区根)。
24
- * 全局安装时运行 `scripts/install-opencode-plugin.ps1` / `install-opencode-plugin.sh` 会默认写入 **`OHAI_REPORT_CLI`**
25
- * (Windows 用户环境变量;Linux `~/.config/environment.d/`;macOS 追加 `~/.zprofile`),也可用 `-SkipEnv` / `--skip-env` 跳过。
26
- *
27
- * 公司邮箱:安装脚本写入全局 ``~/.config/opencode/ohai-report/email.json``(字段 ``user_email``);
28
- * 可选仓库级 ``<project>/.ohai-report/user_email.json`` 覆盖。插件在无 Langfuse id 时注入 ``OHAI_USER_EMAIL`` / ``OHAI_USER_ID``。
29
- *
30
- * **远端 GitCode/Gitee/Webhook 建单(`report_ai_issue`)**:宿主环境变量 ``OHAI_REPORT_SINK``:
31
- * - 不设:默认追加 ``--sink webhook``,让 `/report-ai-issue` 执行远端上报。
32
- * - ``local``:``create`` 只写本地 ``.ohai-report/issues/``(与 CLI 默认一致)。
33
- * - ``gitcode``:追加 ``--sink gitcode``(默认仓库见 ``tools/ohai-report/ohai_report/gitcode_defaults.py``,可用 ``OHAI_GITCODE_OWNER`` / ``OHAI_GITCODE_REPO`` 覆盖);需配置 ``OHAI_GITCODE_TOKEN``(或 ``GITCODE_ACCESS_TOKEN``)。Gitee 请设 ``OHAI_GITCODE_API_BASE=https://gitee.com/api/v5``。
34
- * - 维护者自动建仓库标签并绑定 Issue:另设 ``OHAI_GITCODE_LABEL_MAINTAINER=1``(或 ``OHAI_GITCODE_MAINTAINER=1``),插件会追加 ``--gitcode-label-maintainer``。
35
- * - ``webhook``:追加 ``--sink webhook``(``WEBHOOK_URL`` / ``OHAI_WEBHOOK_URL`` 等见 README)。POST 体中 ``labels`` 由 Python 侧以**逗号分隔字符串**发送,并含 ``tool:``/``model:``/``level:``/``category:`` 自动合并。
36
- */
37
-
38
- function truthyEnv(v: string | undefined): boolean {
39
- const t = (v ?? "").trim().toLowerCase()
40
- return t === "1" || t === "true" || t === "yes" || t === "on"
41
- }
42
-
43
- function webhookTimeoutArgv(env: Record<string, string | undefined>): string[] {
44
- const raw = (env.OHAI_WEBHOOK_TIMEOUT ?? env.WEBHOOK_TIMEOUT ?? "").trim()
45
- const timeout = raw ? Number(raw) : 10
46
- if (!Number.isFinite(timeout) || timeout <= 0) return ["--webhook-timeout", "10"]
47
- return ["--webhook-timeout", String(timeout)]
48
- }
49
-
50
- /** `/report-ai-issue` 默认远端上报;只有显式 `OHAI_REPORT_SINK=local` 才使用 CLI 默认(local)。 */
51
- function ohaiReportCreateSinkArgv(): string[] {
52
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
53
- const sink = (env.OHAI_REPORT_SINK ?? "").trim().toLowerCase()
54
- if (sink === "local") return []
55
- if (sink === "gitcode") {
56
- const out = ["--sink", "gitcode"]
57
- if (truthyEnv(env.OHAI_GITCODE_LABEL_MAINTAINER) || truthyEnv(env.OHAI_GITCODE_MAINTAINER)) {
58
- out.push("--gitcode-label-maintainer")
59
- }
60
- return out
61
- }
62
- if (sink && sink !== "webhook") {
63
- console.warn(`[oh-ai-report] Unsupported OHAI_REPORT_SINK=${sink}; falling back to webhook.`)
64
- }
65
- return ["--sink", "webhook", ...webhookTimeoutArgv(env)]
66
- }
67
-
68
- /**
69
- * OpenCode 全局配置目录(与安装脚本一致):`%USERPROFILE%\.config\opencode` 或 `$XDG_CONFIG_HOME/opencode`。
70
- */
71
- function opencodeGlobalConfigDir(): string {
72
- const proc = (globalThis as { process?: { platform?: string; env?: Record<string, string | undefined> } }).process
73
- const env = proc?.env ?? {}
74
- const xdg = (env.XDG_CONFIG_HOME ?? "").trim()
75
- if (xdg) return join(xdg, "opencode")
76
- if (proc?.platform === "win32") {
77
- const base = (env.USERPROFILE ?? "").trim() || homedir()
78
- return join(base, ".config", "opencode")
79
- }
80
- return join((env.XDG_CONFIG_HOME ?? "").trim() || join(homedir(), ".config"), "opencode")
81
- }
82
-
83
- /** 读取安装脚本写入的独立邮箱 JSON(新路径优先,兼容旧文件)。 */
84
- function readUserEmailFromInstallJson(): string {
85
- const oc = opencodeGlobalConfigDir()
86
- const paths = [join(oc, "ohai-report", "email.json"), join(oc, "ohai-report-user.json")]
87
- for (const p of paths) {
88
- try {
89
- if (!existsSync(p)) continue
90
- const raw = readFileSync(p, "utf8")
91
- const j = JSON.parse(raw) as Record<string, unknown>
92
- const e = j.user_email ?? j.userEmail
93
- if (typeof e === "string" && e.trim()) return e.trim()
94
- } catch {
95
- continue
96
- }
97
- }
98
- return ""
99
- }
100
-
101
- /**
102
- * 解析 `ohai-report` 入口脚本路径(供全局安装插件 + 任意工作区使用)。
103
- */
104
- function ohaiReportPyPath(): string {
105
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
106
- const p = (env.OHAI_REPORT_CLI ?? "").trim()
107
- return p || "tools/ohai-report/ohai_report.py"
108
- }
109
-
110
- function isUsableReportCwd(value: string): boolean {
111
- const t = (value || "").trim()
112
- if (!t) return false
113
- const full = normalize(resolve(t))
114
- const root = normalize(parse(full).root)
115
- return full !== root
116
- }
117
-
118
- function repoRootFromOhaiReportCli(): string {
119
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
120
- const raw = (env.OHAI_REPORT_CLI ?? "").trim()
121
- if (!raw) return ""
122
- const py = normalize(resolve(raw))
123
- if (!existsSync(py)) return ""
124
-
125
- const scriptDir = normalize(dirname(py))
126
- const asPosix = scriptDir.replace(/\\/g, "/")
127
- if (asPosix.endsWith("/tools/ohai-report")) {
128
- const root = dirname(dirname(scriptDir))
129
- return isUsableReportCwd(root) ? root : ""
130
- }
131
- return ""
132
- }
133
-
134
- function resolveReportCwd(): string {
135
- return repoRootFromOhaiReportCli()
136
- }
137
-
138
- /**
139
- * Bus 事件形如 `{ id, type, properties }`,业务字段多在 `properties` 内;
140
- * 工具 execute 的 context 多在顶层。`properties.info` 单独成层便于取 model / 扩展字段。
141
- */
142
- function busPayloadLayers(ctx: unknown): Record<string, unknown>[] {
143
- if (!ctx || typeof ctx !== "object") return []
144
- const o = ctx as Record<string, unknown>
145
- const layers: Record<string, unknown>[] = [o]
146
- const props = o.properties
147
- if (props && typeof props === "object") {
148
- const p = props as Record<string, unknown>
149
- layers.push(p)
150
- const info = p.info
151
- if (info && typeof info === "object") layers.push(info as Record<string, unknown>)
152
- }
153
- const rootInfo = o.info
154
- if (rootInfo && typeof rootInfo === "object") layers.push(rootInfo as Record<string, unknown>)
155
- return layers
156
- }
157
-
158
- function pickStringFromRecord(obj: Record<string, unknown>, keys: readonly string[]): string {
159
- for (const k of keys) {
160
- const v = obj[k]
161
- if (typeof v === "string" && v.trim()) return v.trim()
162
- }
163
- return ""
164
- }
165
-
166
- /** 从单层对象及常见嵌套对象(metadata / tracing / telemetry 等)收集 tracing 相关字符串。 */
167
- function collectTracingFromObject(obj: Record<string, unknown>): {
168
- userId: string
169
- traceId: string
170
- observationId: string
171
- } {
172
- let userId = pickStringFromRecord(obj, ["userId", "userID", "user_id", "tracingUserId", "tracing_user_id"])
173
- let traceId = pickStringFromRecord(obj, [
174
- "traceId",
175
- "traceID",
176
- "trace_id",
177
- "langfuseTraceId",
178
- "langfuse_trace_id",
179
- "otelTraceId",
180
- "otel_trace_id",
181
- ])
182
- let observationId = pickStringFromRecord(obj, [
183
- "observationId",
184
- "observationID",
185
- "observation_id",
186
- "spanId",
187
- "span_id",
188
- "langfuseObservationId",
189
- "langfuse_observation_id",
190
- ])
191
- const nestedKeys = ["metadata", "tracing", "telemetry", "langfuse", "experimentalTelemetry", "otel", "observability"]
192
- for (const nk of nestedKeys) {
193
- const sub = obj[nk]
194
- if (sub && typeof sub === "object" && !Array.isArray(sub)) {
195
- const inner = collectTracingFromObject(sub as Record<string, unknown>)
196
- if (!userId && inner.userId) userId = inner.userId
197
- if (!traceId && inner.traceId) traceId = inner.traceId
198
- if (!observationId && inner.observationId) observationId = inner.observationId
199
- }
200
- }
201
- return { userId, traceId, observationId }
202
- }
203
-
204
- function pickTracing(ctx: unknown): { userId: string; traceId: string; observationId: string } {
205
- let userId = ""
206
- let traceId = ""
207
- let observationId = ""
208
- for (const layer of busPayloadLayers(ctx)) {
209
- const t = collectTracingFromObject(layer)
210
- if (!userId && t.userId) userId = t.userId
211
- if (!traceId && t.traceId) traceId = t.traceId
212
- if (!observationId && t.observationId) observationId = t.observationId
213
- }
214
- return { userId, traceId, observationId }
215
- }
216
-
217
- /** W3C `traceparent`:向子进程传播 OTEL 时常用,可从环境变量补 trace / parent span。 */
218
- function parseTraceparent(tp: string): { traceId: string; parentSpanId: string } {
219
- const parts = tp.trim().split("-")
220
- if (parts.length < 4) return { traceId: "", parentSpanId: "" }
221
- const ver = parts[0] ?? ""
222
- const trace = (parts[1] ?? "").trim()
223
- const span = (parts[2] ?? "").trim()
224
- if (!/^[0-9a-f]{2}$/i.test(ver)) return { traceId: "", parentSpanId: "" }
225
- if (!/^[0-9a-f]{32}$/i.test(trace)) return { traceId: "", parentSpanId: "" }
226
- if (!/^[0-9a-f]{16}$/i.test(span)) return { traceId: trace.toLowerCase(), parentSpanId: "" }
227
- return { traceId: trace.toLowerCase(), parentSpanId: span.toLowerCase() }
228
- }
229
-
230
- /**
231
- * OpenCode 与 Langfuse/OTEL 常在「宿主进程环境变量」中携带 trace,而不在 bus payload。
232
- * 插件与 Python 子进程同继承该环境时,此处显式读出并传给 metadata update / create。
233
- */
234
- function pickTracingFromHostEnv(): { userId: string; traceId: string; observationId: string } {
235
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
236
- const pick = (...keys: string[]) => {
237
- for (const k of keys) {
238
- const v = env[k]?.trim()
239
- if (v) return v
240
- }
241
- return ""
242
- }
243
- let traceId = pick(
244
- "LANGFUSE_TRACE_ID",
245
- "TRACE_ID",
246
- "OTEL_TRACE_ID",
247
- "LANGFUSE_ROOT_TRACE_ID",
248
- )
249
- let observationId = pick(
250
- "LANGFUSE_OBSERVATION_ID",
251
- "OHAI_OBSERVATION_ID",
252
- "OBSERVATION_ID",
253
- "OTEL_SPAN_ID",
254
- )
255
- const userId = pick("OHAI_USER_ID", "LANGFUSE_USER_ID", "OPENCODE_USER_ID")
256
- const tp = pick("TRACEPARENT", "traceparent")
257
- if (tp) {
258
- const { traceId: t2, parentSpanId } = parseTraceparent(tp)
259
- if (!traceId && t2) traceId = t2
260
- if (!observationId && parentSpanId) observationId = parentSpanId
261
- }
262
- return { userId, traceId, observationId }
263
- }
264
-
265
- /** OpenCode 工具 context / Bus 事件上可拿到的会话标识(非 LLM 模型名)。 */
266
- function pickSessionId(ctx: unknown): string {
267
- for (const layer of busPayloadLayers(ctx)) {
268
- for (const key of ["sessionID", "sessionId"] as const) {
269
- const v = layer[key]
270
- if (typeof v === "string" && v.trim()) return v.trim()
271
- }
272
- const session = layer.session
273
- if (session && typeof session === "object") {
274
- const id = (session as { id?: unknown }).id
275
- if (typeof id === "string" && id.trim()) return id.trim()
276
- }
277
- const id = layer.id
278
- if (typeof id === "string" && id.trim() && layer.slug !== undefined) return id.trim()
279
- }
280
- return ""
281
- }
282
-
283
- function pickModelIdField(model: unknown): string {
284
- if (typeof model === "string" && model.trim()) return model.trim()
285
- if (model && typeof model === "object") {
286
- const o = model as Record<string, unknown>
287
- const id = o.id
288
- if (typeof id === "string" && id.trim()) return id.trim()
289
- }
290
- return ""
291
- }
292
-
293
- /** OpenCode 当前会话选用的 LLM(各层上的 model,取 model.id 或字符串;含 properties.info)。 */
294
- function pickOpenCodeModel(ctx: unknown): string {
295
- for (const layer of busPayloadLayers(ctx)) {
296
- const id = pickModelIdField(layer.model)
297
- if (id) return id
298
- }
299
- return ""
300
- }
301
-
302
- async function fetchSessionRecord(client: unknown, sessionId: string): Promise<Record<string, unknown> | null> {
303
- if (!sessionId.trim() || !client || typeof client !== "object") return null
304
- const session = (client as { session?: { get?: (a: unknown) => Promise<unknown> } }).session
305
- if (!session?.get) return null
306
- try {
307
- const res = await session.get({ path: { id: sessionId } } as never)
308
- if (!res || typeof res !== "object") return null
309
- const o = res as Record<string, unknown>
310
- if (o.data && typeof o.data === "object") return o.data as Record<string, unknown>
311
- return o
312
- } catch {
313
- return null
314
- }
315
- }
316
-
317
- async function resolveTracing(
318
- client: unknown,
319
- ctx: unknown,
320
- sessionId: string,
321
- ): Promise<{ userId: string; traceId: string; observationId: string }> {
322
- let { userId, traceId, observationId } = pickTracing(ctx)
323
- const remote = await fetchSessionRecord(client, sessionId)
324
- if (remote) {
325
- const t = collectTracingFromObject(remote)
326
- if (!userId && t.userId) userId = t.userId
327
- if (!traceId && t.traceId) traceId = t.traceId
328
- if (!observationId && t.observationId) observationId = t.observationId
329
- }
330
- const host = pickTracingFromHostEnv()
331
- if (!userId && host.userId) userId = host.userId
332
- if (!traceId && host.traceId) traceId = host.traceId
333
- if (!observationId && host.observationId) observationId = host.observationId
334
- return { userId, traceId, observationId }
335
- }
336
-
337
- function debugOhaiPlugin(
338
- label: string,
339
- payload: unknown,
340
- fields: {
341
- sessionId: string
342
- model: string
343
- userId: string
344
- traceId: string
345
- observationId: string
346
- },
347
- ) {
348
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
349
- const flag = env?.OHAI_REPORT_PLUGIN_DEBUG?.trim()
350
- if (!flag || flag === "0" || flag.toLowerCase() === "false") return
351
- const o = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : null
352
- const top = o ? Object.keys(o).sort().join(",") : ""
353
- const props =
354
- o?.properties && typeof o.properties === "object"
355
- ? Object.keys(o.properties as object).sort().join(",")
356
- : ""
357
- const type = typeof o?.type === "string" ? o.type : ""
358
- console.error(
359
- `[oh-ai-report] ${label} type=${type} sessionId=${fields.sessionId || "(empty)"} model=${fields.model || "(empty)"} userId=${fields.userId || "(empty)"} traceId=${fields.traceId || "(empty)"} observationId=${fields.observationId || "(empty)"} keys=[${top}] propertiesKeys=[${props}]`,
360
- )
361
- }
362
-
363
- /** OpenCode 将 `opencode.json` 里 `plugin_config[插件项]` 作为第二参数传入。 */
364
- function pickEmployeeIdFromPluginConfig(options: unknown): string {
365
- if (!options || typeof options !== "object") return ""
366
- const o = options as Record<string, unknown>
367
- const keys = ["employeeId", "employee_id", "ohaiEmployeeId", "OHAI_EMPLOYEE_ID", "opencodeEmployeeId"] as const
368
- for (const k of keys) {
369
- const v = o[k]
370
- if (typeof v === "string" && v.trim()) return v.trim()
371
- if (typeof v === "number" && Number.isFinite(v)) return String(v)
372
- }
373
- return ""
374
- }
375
-
376
- export const OHAIReportPlugin: Plugin = async ({ $, client }, options) => {
377
- const configuredEmployeeId = pickEmployeeIdFromPluginConfig(options)
378
- const installedUserEmail = readUserEmailFromInstallJson()
379
- const reportCwd = resolveReportCwd()
380
-
381
- const hostProcessEnv = (): Record<string, string | undefined> =>
382
- (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
383
-
384
- /**
385
- * 工号:`plugin_config` 或环境变量 `OHAI_EMPLOYEE_ID` / `OPENCODE_EMPLOYEE_ID`;
386
- * 插件在调用 Python 时注入 `OHAI_USER_NAME` / `OHAI_USER_ID`(仅当宿主未显式设置对应变量时)。
387
- */
388
- const employeeEnvForSubprocess = (): Record<string, string> => {
389
- const host = hostProcessEnv()
390
- const eid = (
391
- configuredEmployeeId ||
392
- (host.OHAI_EMPLOYEE_ID ?? "").trim() ||
393
- (host.OPENCODE_EMPLOYEE_ID ?? "").trim()
394
- ).trim()
395
- if (!eid) return {}
396
- const out: Record<string, string> = {}
397
- if (!(host.OHAI_USER_ID ?? "").trim()) out.OHAI_USER_ID = eid
398
- if (!(host.OHAI_USER_NAME ?? "").trim()) out.OHAI_USER_NAME = eid
399
- return out
400
- }
401
-
402
- /** 将 tracing 与工号相关变量写入子进程环境,便于 `metadata update` / `create --metadata auto` 读取。 */
403
- const withOhaiSubprocessEnv = (
404
- proc: { env: (e: Record<string, string>) => unknown; text: (enc?: string) => Promise<string> },
405
- tracing: { userId: string; traceId: string; observationId: string; sessionId?: string },
406
- ) => {
407
- const extra: Record<string, string> = { ...employeeEnvForSubprocess() }
408
- if (reportCwd) extra.OHAI_REPORT_CWD = reportCwd
409
- if ((tracing.sessionId ?? "").trim()) extra.OHAI_SESSION_ID = (tracing.sessionId ?? "").trim()
410
- if (tracing.traceId.trim()) extra.LANGFUSE_TRACE_ID = tracing.traceId.trim()
411
- if (tracing.observationId.trim()) extra.LANGFUSE_OBSERVATION_ID = tracing.observationId.trim()
412
- if (tracing.userId.trim()) extra.OHAI_USER_ID = tracing.userId.trim()
413
- const host = hostProcessEnv()
414
- const resolvedUid = ((extra.OHAI_USER_ID ?? host.OHAI_USER_ID) ?? "").trim()
415
- if (!resolvedUid && installedUserEmail.trim()) {
416
- extra.OHAI_USER_ID = installedUserEmail.trim()
417
- if (!(host.OHAI_USER_NAME ?? "").trim() && !(extra.OHAI_USER_NAME ?? "").trim()) {
418
- extra.OHAI_USER_NAME = installedUserEmail.trim()
419
- }
420
- }
421
- if (installedUserEmail.trim() && !(host.OHAI_USER_EMAIL ?? "").trim()) {
422
- extra.OHAI_USER_EMAIL = installedUserEmail.trim()
423
- }
424
- return Object.keys(extra).length ? proc.env(extra) : proc
425
- }
426
-
427
- const flushMetadata = async (
428
- sessionId: string,
429
- model: string,
430
- userId: string,
431
- traceId: string,
432
- observationId: string,
433
- ) => {
434
- if (!reportCwd) return
435
- try {
436
- const tracing = { userId, traceId, observationId, sessionId }
437
- const py = ohaiReportPyPath()
438
- const proc = $`python ${py} metadata update --source opencode --cwd ${reportCwd} --session-id ${sessionId} --model ${model} --user-id ${userId} --trace-id ${traceId} --observation-id ${observationId} --drop-message-and-subagent`
439
- await withOhaiSubprocessEnv(proc as never, tracing)
440
- } catch {
441
- // Metadata refresh is best-effort; issue creation still works without it.
442
- }
443
- }
444
-
445
- const refresh = async (
446
- label: string,
447
- payload: unknown,
448
- sessionId: string,
449
- ): Promise<{ userId: string; traceId: string; observationId: string }> => {
450
- const model = pickOpenCodeModel(payload)
451
- const tracing = await resolveTracing(client, payload, sessionId)
452
- debugOhaiPlugin(label, payload, {
453
- sessionId,
454
- model,
455
- userId: tracing.userId,
456
- traceId: tracing.traceId,
457
- observationId: tracing.observationId,
458
- })
459
- await flushMetadata(
460
- sessionId,
461
- model,
462
- tracing.userId.trim() || installedUserEmail.trim(),
463
- tracing.traceId,
464
- tracing.observationId,
465
- )
466
- return tracing
467
- }
468
-
469
- return {
470
- event: async ({ event }) => {
471
- if (
472
- event.type === "session.created" ||
473
- event.type === "session.updated" ||
474
- event.type === "command.executed"
475
- ) {
476
- const sessionId = pickSessionId(event)
477
- await refresh("event", event, sessionId)
478
- }
479
- },
480
-
481
- tool: {
482
- report_ai_issue: tool({
483
- description:
484
- "Report an AI development issue. Do not include full logs; session/trace IDs appear in the template log section when metadata is available.",
485
- args: {
486
- title: tool.schema.string(),
487
- category: tool.schema.string(),
488
- summary: tool.schema.string(),
489
- expected_behavior: tool.schema.string(),
490
- actual_behavior: tool.schema.string(),
491
- severity: tool.schema.string().optional(),
492
- user_description: tool.schema.string(),
493
- },
494
- async execute(args, context) {
495
- if (!reportCwd) {
496
- return JSON.stringify({
497
- ok: false,
498
- report_error: "invalid_report_cwd",
499
- report_error_detail:
500
- "OHAI_REPORT_CLI is not set to a valid .../tools/ohai-report/ohai_report.py path, so the repository root for .ohai-report/issues cannot be derived.",
501
- })
502
- }
503
- const sessionId = pickSessionId(context)
504
- const tracing = await refresh("tool.report_ai_issue", context, sessionId)
505
-
506
- const py = ohaiReportPyPath()
507
- const sinkArgv = ohaiReportCreateSinkArgv()
508
- const proc = withOhaiSubprocessEnv(
509
- $`python ${py} create \
510
- --source opencode \
511
- --cwd ${reportCwd} \
512
- --title ${args.title} \
513
- --category ${args.category} \
514
- --summary ${args.summary} \
515
- --expected-behavior ${args.expected_behavior} \
516
- --actual-behavior ${args.actual_behavior} \
517
- --severity ${args.severity ?? "P2"} \
518
- --user-description ${args.user_description} \
519
- --metadata auto \
520
- --field agent=opencode \
521
- ${sinkArgv} \
522
- --quiet \
523
- --json` as never,
524
- { ...tracing, sessionId },
525
- )
526
- const out = (await proc) as { text: () => Promise<string> }
527
- const text = await out.text()
528
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
529
- const last = lines[lines.length - 1] ?? ""
530
- try {
531
- const result = JSON.parse(last) as Record<string, unknown>
532
- if (result && typeof result === "object" && !Array.isArray(result)) {
533
- const reportedSession = typeof result.session_id === "string" ? result.session_id.trim() : ""
534
- const status = typeof result.status === "string" ? result.status.trim() : ""
535
- const webhookUrl = typeof result.webhook_url === "string" ? result.webhook_url.trim() : ""
536
- const gitcodeUrl = typeof result.gitcode_url === "string" ? result.gitcode_url.trim() : ""
537
- const remoteCreated = Boolean(webhookUrl || gitcodeUrl || status === "created-webhook" || status === "created-gitcode")
538
- result.ok = remoteCreated
539
- if (!remoteCreated) {
540
- result.report_error = "remote_url_missing"
541
- result.report_error_detail =
542
- "CLI completed without a webhook_url/gitcode_url or remote-created status; do not report this as a successful remote issue."
543
- }
544
- result.session_id_source = sessionId.trim()
545
- ? "opencode_context"
546
- : reportedSession
547
- ? "metadata"
548
- : "missing"
549
- if (sessionId.trim()) result.opencode_context_session_id = sessionId.trim()
550
- if (!sessionId.trim() && !reportedSession) {
551
- result.session_id_warning =
552
- "OpenCode tool context did not include a session id, and no fresh metadata session was available."
553
- }
554
- return JSON.stringify(result)
555
- }
556
- } catch {
557
- // Preserve the original CLI output if it is not JSON.
558
- }
559
- return JSON.stringify({
560
- ok: false,
561
- report_error: "non_json_cli_output",
562
- report_error_detail: "ohai-report CLI did not return a parseable JSON line; remote issue status is unknown.",
563
- raw_output: text,
564
- })
565
- },
566
- }),
567
- },
568
- }
569
- }
@@ -1,45 +0,0 @@
1
- # problem_collect
2
-
3
- OpenCode 场景下的 AI 辅助开发问题采集与本地、Webhook、GitCode 上报工具。
4
-
5
- | 目录 / 文件 | 说明 |
6
- | --- | --- |
7
- | `docs/` | 架构设计、最佳实践和阶段总结文档 |
8
- | `tools/ohai-report/` | `ohai-report` Python CLI |
9
- | `examples/` | 示例输入输出 |
10
- | `scripts/` | 安装脚本和辅助脚本 |
11
- | `.opencode/` | OpenCode 命令与插件 |
12
- | `.claude/` | Claude Code 命令 |
13
- | `.ohai-report/` | 运行期数据,默认被 `.gitignore` 忽略 |
14
-
15
- ## npx 安装
16
-
17
- 发布到 npm 后可直接使用:
18
-
19
- ```bash
20
- npx opencode-ohai-report install opencode --email user@company.com
21
- npx opencode-ohai-report install claude --email user@company.com
22
- npx opencode-ohai-report install both --email user@company.com
23
- ```
24
-
25
- 安装器会把随包发布的 runtime 复制到 `~/.config/ohai-report/runtime`,再把 `OHAI_REPORT_CLI` 指向这个稳定位置,避免 OpenCode/Claude 重启后依赖临时的 npx 缓存目录。
26
-
27
- 调试与校验:
28
-
29
- ```bash
30
- npx opencode-ohai-report doctor
31
- npx opencode-ohai-report create --source opencode --issue-file examples/issue-input.json --metadata auto --json
32
- ```
33
-
34
- ## 常用命令
35
-
36
- ```bash
37
- # OpenCode /report-ai-issue 默认使用 webhook;可通过 WEBHOOK_URL 或 OHAI_WEBHOOK_URL 覆盖。
38
- python tools/ohai-report/ohai_report.py create --metadata auto --sink webhook --issue-file examples/issue-input.json --json
39
- python tools/ohai-report/ohai_report.py create --metadata auto --sink gitcode --issue-file examples/issue-input.json --json
40
- python scripts/create_issue.py
41
- ```
42
-
43
- Webhook URL 解析顺序:CLI `--webhook-url` -> 环境变量 `OHAI_WEBHOOK_URL` / `WEBHOOK_URL` -> `tools/ohai-report/ohai_report/webhook_defaults.py` 中的 `DEFAULT_WEBHOOK_URL`。
44
-
45
- GitCode 可选配置见 `tools/ohai-report/ohai_report/gitcode_defaults.py`,也可以用 `OHAI_GITCODE_OWNER`、`OHAI_GITCODE_REPO`、`OHAI_GITCODE_TOKEN` 等环境变量覆盖。