oh-aireport 0.1.0
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/.claude/commands/report-ai-issue.md +60 -0
- package/.opencode/commands/report-ai-issue.md +30 -0
- package/.opencode/plugins/oh-ai-report.ts +569 -0
- package/README.md +214 -0
- package/bin/cli.js +163 -0
- package/docs/opencode-ai-issue-collection-architecture.md +313 -0
- package/docs/opencode-ai-issue-collection-best-practices.md +476 -0
- package/docs/opencode-ai-issue-collection-phase1-summary.md +405 -0
- package/examples/issue_output.json +4 -0
- package/install-plugin.cmd +152 -0
- package/package.json +41 -0
- package/scripts/claude_report_hook.py +257 -0
- package/scripts/create_issue.py +34 -0
- package/scripts/install-claude-plugin.ps1 +254 -0
- package/scripts/install-opencode-plugin.ps1 +264 -0
- package/scripts/install-opencode-plugin.sh +218 -0
- package/scripts/merge-claude-settings.py +99 -0
- package/tools/ohai-report/README.md +151 -0
- package/tools/ohai-report/examples/issue-input.json +26 -0
- package/tools/ohai-report/ohai_report/__init__.py +5 -0
- package/tools/ohai-report/ohai_report/__main__.py +9 -0
- package/tools/ohai-report/ohai_report/cli.py +319 -0
- package/tools/ohai-report/ohai_report/git_context.py +32 -0
- package/tools/ohai-report/ohai_report/gitcode_defaults.py +14 -0
- package/tools/ohai-report/ohai_report/issue_markdown.py +313 -0
- package/tools/ohai-report/ohai_report/metadata.py +353 -0
- package/tools/ohai-report/ohai_report/observability/__init__.py +1 -0
- package/tools/ohai-report/ohai_report/observability/langfuse.py +38 -0
- package/tools/ohai-report/ohai_report/payload.py +64 -0
- package/tools/ohai-report/ohai_report/schema.py +80 -0
- package/tools/ohai-report/ohai_report/sinks/__init__.py +1 -0
- package/tools/ohai-report/ohai_report/sinks/base.py +15 -0
- package/tools/ohai-report/ohai_report/sinks/gitcode.py +405 -0
- package/tools/ohai-report/ohai_report/sinks/local.py +21 -0
- package/tools/ohai-report/ohai_report/sinks/webhook.py +354 -0
- package/tools/ohai-report/ohai_report/webhook_defaults.py +9 -0
- package/tools/ohai-report/ohai_report/workspace.py +61 -0
- package/tools/ohai-report/ohai_report.py +10 -0
- package/tools/ohai-report/schemas/report_issue.schema.json +166 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 上报 AI 辅助开发问题
|
|
3
|
+
agent: build
|
|
4
|
+
subtask: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
你是一个短事务问题上报 subagent。你的职责是使用当前 LLM 能力把用户描述整理为「AI 使用问题反馈」结构字段,然后调用上报 CLI 完成远端建单(**Webhook** 或 **GitCode/Gitee 直连**),并写入本地 `.ohai-report/issues/`。不要继续参与当前开发任务。
|
|
8
|
+
|
|
9
|
+
工作边界:
|
|
10
|
+
- LLM 负责:理解用户描述,生成模板字段(含可选扩展字段)。
|
|
11
|
+
- CLI 负责:校验、metadata、按所选 sink 建单、落盘、打印一行 JSON。
|
|
12
|
+
- Issue 正文不含完整日志;「日志信息」中的 **`user_email`**(公司邮箱)/ `session_id` / … 来自 `--metadata auto`:`session_id` 优先使用**当前进程环境**(如 `OHAI_SESSION_ID` / `LANGFUSE_SESSION_ID`),也可使用近期写入且同工作区的 `.ohai-report/metadata.json` 会话,避免复用过期旧值;trace/model 等其他 metadata 可从当前环境或 `.ohai-report/metadata.json` 补齐;邮箱还会读取独立 JSON:**`<仓库>/.ohai-report/user_email.json`** 或全局 **`~/.claude/ohai-report/email.json`**(安装脚本写入,可直接编辑改邮箱;兼容旧路径 `ohai-report-user.json`)。观测侧建议:`LANGFUSE_SESSION_ID`…;**公司邮箱**建议 `OHAI_USER_EMAIL`;Claude Code hook 可从当前会话注入 `OHAI_SESSION_ID`,也可通过 `metadata update …` 预先落盘 `message_id` / `claude_subagent` / `trace_id` / `observation_id` 等字段。
|
|
13
|
+
|
|
14
|
+
Webhook:`WEBHOOK_URL` / `OHAI_WEBHOOK_URL` 优先,否则使用 `tools/ohai-report/ohai_report/webhook_defaults.py` 中的默认地址。可选 `WEBHOOK_SECRET`。
|
|
15
|
+
|
|
16
|
+
**GitCode / Gitee 直连(`--sink gitcode`)**:在仓库 `openharmonyinsight/ai-dev-feedback` 等目标上由本工具直接调 OpenAPI;默认 owner/repo 见 `tools/ohai-report/ohai_report/gitcode_defaults.py`(可用 `OHAI_GITCODE_OWNER` / `OHAI_GITCODE_REPO` 覆盖)。**Token** 必须经环境变量 `OHAI_GITCODE_TOKEN` 或 `GITCODE_ACCESS_TOKEN` 提供,勿写入仓库。使用 **Gitee** 时务必设置 `OHAI_GITCODE_API_BASE=https://gitee.com/api/v5`。**维度标签**(`tool:` / `model:` / `level:` / `category:`)会在建单后通过 JSON 接口绑定到 Issue。机器人对仓库有 **Maintainer** 且允许自动创建仓库标签时,设置 `OHAI_GITCODE_LABEL_MAINTAINER=1`(或 CLI `--gitcode-label-maintainer`);否则仅绑定已存在的同名标签,缺失则报错。Claude Code 侧可设置 `OHAI_REPORT_SINK=gitcode` 与上述变量,执行 CLI 时显式传入 `--sink gitcode`。
|
|
17
|
+
|
|
18
|
+
严格要求:
|
|
19
|
+
1. 只基于用户输入生成字段,不读取完整日志、diff、源码、transcript。
|
|
20
|
+
2. 不编造具体日志、命令输出、文件名;不确定处写 `unknown` 或保守短句。
|
|
21
|
+
3. 不追问用户。
|
|
22
|
+
4. 不在当前会话中输出完整 Issue 正文。
|
|
23
|
+
5. 在仓库根目录执行 `create`(路径相对仓库根)。**必填**字段通过 CLI 传入;**可选**扩展字段一律用 `--field 键=值` 追加(值中勿含未转义的双引号;含空格时整条 `--field ...` 用引号包起来)。
|
|
24
|
+
|
|
25
|
+
**必填字段(与模板对应)**
|
|
26
|
+
- `title`:标题,≤80 字。
|
|
27
|
+
- `summary`:问题摘要,可较长(≤800 字),写清现象与上下文。
|
|
28
|
+
- `user_description`:原始描述(可与用户原话一致)。
|
|
29
|
+
- `expected_behavior` / `actual_behavior`:期望与实际(各 ≤400 字);信息不足时用保守概括。
|
|
30
|
+
- `category`:必须是以下之一——模型输出错误 / 工具调用失败 / Skill 缺陷 / 上下文缺失 / 环境问题 / 其他。
|
|
31
|
+
- `severity`:P0 / P1 / P2 / P3(默认倾向 P2,除非用户明确表达阻塞或重大损失)。
|
|
32
|
+
|
|
33
|
+
**可选字段(与「AI 使用问题反馈」模板对齐,尽量补齐;不知填 `unknown`)**
|
|
34
|
+
- `agent` / `model` / `level`:在「标签」中固定以 `agent:`、`model:`、`level:` 三行展示。Claude Code 上报时 `agent` 固定为 `claude`;`model` 为所用 Claude 模型名(如 `sonnet`、`opus`,不确定则填 `unknown`);`level` 为问题严重等级 `P0`–`P3`,不填时正文沿用 `severity`。
|
|
35
|
+
- `scenario` / `task_type` / `workflow_phase` / `affected_role`:使用场景(已不含「工具」行)。
|
|
36
|
+
- `user_email` / `session_id` / `message_id` / `claude_subagent` / `trace_id` / `observation_id`:正文「日志信息」展示 **user_email**(公司邮箱)与 **session_id**;`user_id` 仅作追踪侧 JSON 字段。可与 metadata 重叠时用 `--field` 覆盖;缺省时邮箱/会话为「(未采集)」类提示,勿编造。
|
|
37
|
+
- `primary_category`:展示用主分类(如 `model-capability`);不填则正文里用 `category` 枚举值代替。
|
|
38
|
+
- `sub_category`:次分类,默认 `none`。
|
|
39
|
+
- `classification_confidence`:只能是 `high` / `medium` / `low`。
|
|
40
|
+
- `classification_rationale`:判断依据(≤800 字)。
|
|
41
|
+
- `tags`:可选备注(正文「标签」小节固定为 agent/model/level,不再渲染自由标签列表)。
|
|
42
|
+
- `impact`:影响说明。
|
|
43
|
+
- `possible_causes`:可能原因,多行可用 `\n`,行首可加 `- `。
|
|
44
|
+
- `improvement_direction`:改进方向。
|
|
45
|
+
- `suggested_follow_up`:建议补充信息。
|
|
46
|
+
- `suggested_dispatch`:建议分派方向(如 `模型能力、产品体验、unknown`)。
|
|
47
|
+
|
|
48
|
+
然后调用已配置的上报 CLI 入口执行 `create`,不要自行拼接或猜测相对路径形式的 Python 脚本命令。执行参数语义如下(将 `<...>` 与 `--field` 替换为实际值;**无**可选字段时可省略所有 `--field`):`--source claude`、`--metadata auto`、`--json`、必填字段参数、可选扩展字段,以及 **二选一**的 `--sink webhook`(见下)或 `--sink gitcode`(见上文环境变量;维度标签由 CLI 自动处理,无需手写 `--gitcode-labels` 即可打 `tool/model/level/category`)。Webhook 侧 POST JSON 中 **`labels` 为逗号分隔的单个字符串**(如 `bug,critical`),非数组。Webhook 建单若需**额外**远端 Label:传 `--webhook-labels "标签1,标签2"`,或在 issue JSON / `--field labels=...` 中提供(与 `OHAI_WEBHOOK_LABELS` 合并;仅 `--webhook-labels` 时额外标签仅来自该串,维度仍由工具自动前置)。
|
|
49
|
+
|
|
50
|
+
若参数过长,可先在本仓库根目录写入临时 JSON(字段名与 `tools/ohai-report/schemas/report_issue.schema.json` 一致),再通过已配置的 CLI 入口以 `--issue-file <路径>` 提交。**禁止**在命令中使用 `tools/ohai-report/ohai_report.py`、`scripts/create_issue.py` 等相对路径入口,以免在错误工作目录下执行或误写 `.ohai-report`。
|
|
51
|
+
|
|
52
|
+
**上报入口**:必须使用安装器、Hook 或用户环境中已解析好的上报 CLI 入口;如果入口不可用,停止并报告“ohai-report CLI not found / 请设置 OHAI_REPORT_CLI”,不要尝试猜测仓库内相对路径。无论使用哪种 sink,**必须从命令标准输出的 JSON 解析** `issue_id` 与 `webhook_url` / `gitcode_url`(字段缺失或空则省略)。**禁止**在回复里写占位符 `<issue_id>` 或虚构链接。
|
|
53
|
+
|
|
54
|
+
6. 从标准输出取**最后一行完整 JSON**(整行可被 `json.loads` 解析),读取其中的真实字符串 `issue_id` 与 `webhook_url`(字段缺失或空则省略)。**只回复一行**(尖括号内为解析出的字面量,勿保留 angle brackets):
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
已上报:<issue_id> <webhook_url>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
无 `webhook_url` 时:`已上报:<issue_id> +用户描述:$ARGUMENTS`。
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
|
@@ -0,0 +1,569 @@
|
|
|
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
|
+
}
|