Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions docs/develop/ai-interaction-sentry-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# AI Interaction Sentry Implementation

本文是 [AI Interaction Sentry](./ai-interaction-sentry.md) 的实现索引,按初始化、Trace Hook、HTTP 请求和资源清理说明当前链路。

## 代码分布

以下 Builder 文件路径以仓库根目录为基准。

| 文件 | 职责 |
| --- | --- |
| `tools/ai/ai.go` | 执行 Think 和历史归档,在一次交互开始时保存 Transport |
| `tools/ai/transport_context.go` | 通过 context 保存和读取 extra headers 的副本 |
| `tools/ai/wasmtrans/wasmtrans.go` | 构造 HTTP 请求、调用原生 fetch、读取和解析响应 |
| `tools/ai/wasmtrans/headers.go` | 合并 extra headers,保护凭据和 HTTP 控制字段 |
| `tools/ai/wasmtrans/promise.go` | 等待 JS Promise,返回结果、拒绝原因或 context 错误 |
| `tools/ai/transport.go` | 定义 Transport 契约,以及客户端错误和可重试错误的分类 |
| `tools/ispx/ai.go` | 接收 AI 配置,组装 `wasmtrans` 和 `traceTransport` |
| `tools/ispx/trace_transport.go` | 在 Interact / Archive 调用前后执行监控,传递 headers 和结束状态 |
| `tools/ispx/trace_hook_wasm.go` | 保存并直接调用页面 Hook,将 JavaScript 异常隔离在监控层 |
| `tools/ispx/trace_transport_test.go` | Trace Transport 的 headers、状态和失败开放行为 |
| `spx-gui/src/ispx/sentry-trace-hook.ts` | 将 Hook 调用转换为独立 Sentry span,并管理活跃 span |
| `spx-gui/src/ispx/sentry-trace-hook.test.ts` | 页面 Hook 的独立 trace、finish 和关闭行为 |
| `spx-gui/src/components/project/runner/ProjectRunner.vue` | 准备游戏和 AI 配置,安装 Hook,管理运行期间的资源清理 |
| `spx-gui/src/setup/sentry.ts` | 初始化 Sentry,配置 AI 请求采样和自动 fetch tracing 过滤 |

配套 Builder backend 的文件路径以其仓库根目录为基准:

| 文件 | 职责 |
| --- | --- |
| `cmd/xbuilder-backend/middleware.go` | 按配置匹配 body 捕获路由,创建并结束 HTTP transaction |
| `internal/tracer/transaction.go` | 续接 trace,保存 metadata、HTTP 状态和有大小限制的 body |
| `internal/tracer/response_writer.go` | 代理响应写入,记录状态、首次写入时间和响应 body 前缀 |
| `internal/tracer/metadata.go` | 提供 context 内并发安全的 tag/extra 记录器 |
| `internal/tracer/httpclient/client.go` | 记录模型 HTTP 请求的 RoundTrip、响应头和传输错误 |
| `internal/aiinteraction/aiinteraction.go` | 读取模型流,累积响应,将 completion ID 放入内部 metadata |
| `internal/aiinteraction/types.go` | 定义内部 metadata 和携带错误 metadata 的 `ErrorWithMetadata` |
| `internal/aiinteraction/tracer.go` | 包装 Interact / Archive,将响应及错误中的 metadata 写入请求 context |
| `internal/controller/controller.go` | 安装 AI interaction 包装 |
| `cmd/xbuilder-backend/util.go` / `slog.go` | 将内部服务器错误原文写入 `error.message`,并给请求错误日志附加 `sentry_trace_id` |
| `internal/config/config.go` / `loader.go` | 定义并读取 Sentry 采样和 body 捕获路由配置 |

## 初始化与接线

1. Go 的 `init()` 注册 `xbuilder_set_trace_hook` JS 入口。
2. `ProjectRunner.prepareAIInteraction()` 为使用 AI 的项目准备描述、endpoint 和 token provider,再创建页面侧 Sentry Trace Hook。
3. `installTraceHook()` 把 `controller.hook` 注入 iframe;如果注入失败,只关闭该 controller,不阻止游戏启动。
4. Go 的 `setTraceHook()` 保存新的回调,再调用 `resetAIDefaultTransport()`。
5. endpoint 和 token provider 齐备后,`resetAIDefaultTransport()` 创建 `wasmtrans`,用当前 Hook 包装成 `traceTransport`,并通过 `ai.SetDefaultTransport()` 安装。

```text
ProjectRunner
-> createSentryTraceHook()
-> iframe.xbuilder_set_trace_hook(controller.hook)
-> Go setTraceHook()
-> resetAIDefaultTransport()
-> traceTransport(wasmtrans)
```

## Trace Hook 契约

页面提供的 Hook 是一个同步回调:

```ts
type TraceHook = (input: { name: string; operation: string }) => {
propagationHeaders: Record<string, string>;
finish(status: "ok" | "error" | "cancelled"): void;
} | null;
```

调用 Hook 时,页面用 `startNewTrace()` 和 `startInactiveSpan()` 创建独立 root,并通过 `getTraceData()` 得到传播请求头。返回的 `finish` 闭包直接捕获本次 span,因此调用方不需要保存 operation ID,也不需要建立双向消息协议。

Go 通过 `syscall/js` 直接调用 Hook,读取字符串类型的传播头,并包装返回的 `finish`。Go 侧的 `sync.Once` 和页面侧的 `finished` 标志共同保证结束操作最多执行一次。

Hook 是可选能力。未安装、已关闭、返回 `null`,或者 Sentry 创建/传播/结束过程抛出异常时,监控层失败开放,AI 请求继续走底层 Transport。

## 一次 HTTP 请求

1. AI 调用保存的 Transport,进入 `traceTransport.Interact()` 或 `Archive()`。
2. 包装层用请求名和 `http.client` 调用 Hook。
3. 页面创建独立 Sentry span,返回 `Sentry-Trace`、`Baggage` 和该 span 对应的 `finish`。
4. 包装层通过 `ai.WithExtraHeaders()` 将 headers 写入原始请求的派生 context;Hook 不可用时跳过这一步。
5. `wasmtrans` 合并 headers,用 `AbortController` 关联请求 context,调用浏览器原生 `fetch`,并解析完整 HTTP 响应。
6. 后端 middleware 续接 trace,业务调用模型,`TracerInteraction` 从返回响应或错误提取 metadata。HTTP handler 返回后,transaction 保存 metadata、HTTP 状态和按配置捕获的 body。
7. Transport 返回后,包装层根据 context 和 error 判定 `ok`、`error` 或 `cancelled`,通过 defer 调用 `finish(status)`。

## 生命周期与并发

- 一个 `traceTransport` 保存创建时的 Hook;一次 Think 及其安排的历史归档沿用保存的 Transport。
- 每次 Hook 调用都创建独立 span,并返回只绑定该 span 的 `finish`,因此并发请求之间不共享 ID 或 pending map。
- Runner 在 stop、rerun、开始新运行、unmount、iframe reload、game error 和 exit 路径关闭当前 controller,并把 iframe 内 Hook 清空。
- controller 关闭时,将仍活跃的 span 结束为 `cancelled`;关闭后的 Hook 对迟到调用返回 `null`。
- 清理 Hook 只结束观测资源。实际网络取消仍由请求 context 和 `wasmtrans` 的 `AbortController` 负责。
- Sentry 状态设置和 span 结束分别容错,状态设置失败时仍会尝试结束 span。

## 后端数据配置

`SENTRY_CAPTURE_BODY_ROUTES` 使用逗号分隔的 `METHOD /path`,例如:

```dotenv
SENTRY_CAPTURE_BODY_ROUTES="POST /ai-interaction/turns,POST /ai-interaction/archives"
```

路由命中时,request 和 response body 分别保存最多 15 KiB,截断标记保存各自完整字节数。`request_id` 从响应或错误的内部 metadata 写入 transaction;`http.first_byte_ms` 从 transaction 开始计时,到首次写响应体时结束。

`ErrorWithMetadata` 保存原始 error 和 metadata 副本,`Error()` 保留错误文本,`Unwrap()` 保留 `errors.Is/As` 的错误链判断。`Interact` 的模型调用和命令参数解析错误出口、`Archive` 的模型调用错误出口通过 `wrapErrorWithMetadata()` 附带已取得的信息;`TracerInteraction` 使用 `errors.As` 提取后沿用 context metadata 记录器。

`replyWithInnerError()` 在映射为内部服务器错误的分支调用 `tracer.SetExtra(ctx, "error.message", err.Error())`。transaction 结束时将完整错误文本写入 span data;该记录独立于 body 捕获配置。HTTP 响应沿用统一的 `code/msg` 格式。

## 验证

在对应目录执行以下静态和单元检查:

```sh
# builder/tools/ai
go test ./...

# builder/tools/ispx
GOOS=js GOARCH=wasm go build ./...
GOOS=js GOARCH=wasm go test .

# builder/spx-gui
pnpm type-check
pnpm exec vitest run src/ispx/sentry-trace-hook.test.ts src/utils/tracing.test.ts
pnpm exec eslint src/ispx/sentry-trace-hook.ts src/ispx/sentry-trace-hook.test.ts src/components/project/runner/ProjectRunner.vue
./build-wasm.sh
pnpm build
```

浏览器联调使用已配置 Sentry 且会执行 Sentry 初始化的运行环境,检查:

1. Interact 和 Archive 请求各自生成对应名称的独立 browser `http.client` root。
2. HTTP 请求带有页面返回的 `Sentry-Trace` / `Baggage`,后端 transaction 使用相同 trace ID。
3. 成功和失败请求分别结束为对应状态,请求 context 结束时走取消处理。
4. 配置 body 捕获后,后端 transaction 保存 API request/response body;超限 body 带有完整字节数标记。
5. 模型调用取得 completion ID 后,成功返回或随后流读取、响应解析失败时,后端 transaction 均包含 `request_id`。
6. 停止、重新运行、卸载、iframe reload、游戏错误和退出后,未完成 span 被清理;并发请求各自结束。
7. 页面 Hook 缺失、安装失败或 Sentry 调用失败时,AI 请求继续通过底层 Transport 执行。
91 changes: 91 additions & 0 deletions docs/develop/ai-interaction-sentry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# AI Interaction Sentry

AI Interaction 的请求监控通过 iSPX Trace Hook 连接页面中的 Sentry 和 WASM 中的 AI Transport。Hook 只负责创建、结束 trace 以及提供传播请求头;AI 请求仍由 `wasmtrans` 使用浏览器原生 `fetch` 发出。

## Trace 结构

每次调用 `Transport.Interact` 或 `Transport.Archive` 时,iSPX 请求页面创建一个独立的 `http.client` root。页面生成的 `Sentry-Trace` 和 `Baggage` 随 HTTP 请求传到后端,后端续接这条 trace。

```text
http.client 浏览器 WASM 请求,独立 root
└── http.server Builder backend 处理请求
└── http.client 后端 HTTP client 调用模型
```

浏览器 root 的名称为 `POST /ai-interaction/turns` 或 `POST /ai-interaction/archives`。每次重试经过 Transport 时创建新的 root,生命周期从调用 Transport 开始持续到 Transport 返回。请求的限流等待发生在调用 Transport 之前。

后端模型 `http.client` 的计时覆盖底层 `RoundTrip` 调用;后端读取模型流、拼装结果和返回 API 响应的过程位于 `http.server` 生命周期内。

## 各层职责

| 层 | 职责 |
| --- | --- |
| `tools/ai` | 执行交互、命令和历史管理,通过 Transport 发起请求 |
| `tools/ispx/trace_transport.go` | 包装 Transport,调用 Trace Hook,将传播请求头放入 context,并在请求返回时结束 trace |
| `tools/ispx/trace_hook_wasm.go` | 保存页面注入的 Hook,完成 Go 与 JavaScript 的直接调用和容错 |
| `spx-gui/src/ispx/sentry-trace-hook.ts` | 创建独立 Sentry span,返回传播请求头和该 span 对应的 `finish` 函数 |
| `wasmtrans` | 合并请求头,执行原生 `fetch`,处理网络取消和响应解析 |
| 后端 HTTP transaction | 续接 trace,记录 HTTP 状态、已配置的请求现场和请求期间积累的 metadata |

Trace Hook 不传输 AI 请求正文或响应正文,也不替代 `wasmtrans`。它只是页面注入给 iSPX 的一个可选回调入口。

## 请求链路

```text
Transport.Interact / Transport.Archive
-> traceTransport 调用页面 Trace Hook
-> 页面创建独立 Sentry span
<- { propagationHeaders, finish }
-> 将传播请求头写入本次请求 context
-> wasmtrans 合并 headers 并调用原生 fetch
-> backend 续接 trace,调用模型并返回响应
-> traceTransport 根据调用结果得到 ok / error / cancelled
-> 调用本次操作对应的 finish(status)
-> 页面更新状态并结束 span
```

这里使用 “Hook” 是因为页面把一个函数注入到 iSPX 的固定扩展点,iSPX 在请求开始时回调它,从而挂接额外的观测逻辑。它不是 React Hook,也不是 WebHook。Hook 返回的 `finish` 闭包已经绑定本次 span,因此不需要 JSON-RPC、请求 ID 或页面侧 pending map。

创建 trace、读取传播信息、设置状态或结束 span 任一步失败时,Hook 都按 best effort 处理,底层 AI 请求继续执行。未安装 Hook 时,直接使用原始 Transport。

传播请求头通过 `ai.WithExtraHeaders` 保存在原始请求的派生 context 中。写入和读取时均复制 map。`wasmtrans` 按大小写不敏感的规则保护 `Authorization`、`Content-Type`、`Content-Length`、`Cookie`、`Host`、`Origin`、`Proxy-Authorization` 和 `Referer`。

## 操作状态

| 结束状态 | 当前 Transport 包装的判定 |
| ----------- | --------------------------------------- |
| `ok` | 请求返回时 context 有效且调用成功 |
| `error` | 请求返回错误且 context 仍有效 |
| `cancelled` | 请求返回时 context 已取消或超过截止时间 |

Go 侧和页面侧都保证每个 `finish` 最多生效一次。Runner 关闭 Hook 时,页面将仍未完成的 span 结束为 `cancelled`。

## 后端请求现场

后端用 `SENTRY_CAPTURE_BODY_ROUTES` 配置需要捕获 body 的路由,按 HTTP 方法和路径精确匹配。AI 接口配置示例:

```dotenv
SENTRY_CAPTURE_BODY_ROUTES="POST /ai-interaction/turns,POST /ai-interaction/archives"
```

命中路由后,后端将 API 的原始请求和响应 body 分别保存为 `request_body`、`response_body`,每份上限为 15 KiB。超过上限时保留前缀,并用 `request_body_dropped` 或 `response_body_dropped` 记录完整 body 的字节数。业务处理仍接收完整请求,客户端仍接收完整响应。

模型调用取得流式响应中的 completion ID 后,通过内部 metadata 将其写为 server transaction 的 `request_id` tag。成功时 metadata 随响应返回;流读取、响应完整性检查或命令参数解析失败时,metadata 随错误返回,由 `TracerInteraction` 统一写入请求 context。模型 HTTP client 还记录响应状态和配置允许的响应头;底层 `RoundTrip` 返回错误时写入 `http.error`。

server transaction 的 `http.first_byte_ms` 记录从 HTTP transaction 开始到首次调用响应体 `Write` 的时间。排查 API 返回内容时查看 `response_body`。统一错误出口 `replyWithInnerError()` 将映射为内部服务器错误的完整 `err.Error()` 写入 transaction 的 `error.message`,独立于 body 捕获配置。服务器错误日志同时记录错误原文,日志中的 `sentry_trace_id` 可用于关联对应 trace。

## 运行生命周期

`ProjectRunner` 为使用 AI Interaction 的运行准备 endpoint、token provider 和 Trace Hook。Go 侧收到 Hook 后,用它包装原生 `wasmtrans`,再安装默认 Transport。

一次 Think 开始时保存当时的 Transport,后续请求及由其安排的历史归档沿用该实例。旧 Transport 即使仍持有旧 Hook,Hook 关闭后也只会返回空结果,AI 请求仍可继续。

停止、重新运行、开始新运行、卸载组件、重载 iframe、游戏错误或退出时,Runner 都会关闭当前 Hook。关闭只清理观测资源,不负责取消 HTTP;网络请求仍通过原始请求 context 和 `AbortController` 取消。

## 采样与查看

页面 Sentry 使用通用的 `VITE_SENTRY_TRACES_SAMPLE_RATE` 配置 AI 请求 root 的采样率。每个 root 使用独立 trace;页面自动 fetch tracing 排除两个 AI URL,由 iSPX 负责创建请求记录。后端启用 tracing,通过 `SENTRY_SAMPLE_RATE` 提供默认采样率,并续接传入的 trace 和采样信息。前后端 Sentry 初始化均依据运行环境执行,开发环境会跳过初始化。

在 Sentry 中按 `POST /ai-interaction/turns` 或 `POST /ai-interaction/archives` 查找浏览器 transaction,沿相同 trace 查看后端请求。浏览器记录展示本次 Transport 调用耗时及状态,后端记录提供 HTTP 处理过程和已配置的 API 请求现场。

代码位置和验证步骤见 [实现说明](./ai-interaction-sentry-implementation.md)。
5 changes: 5 additions & 0 deletions docs/develop/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ See `package.json` or `go.mod` files in subdirectories for details.
- [spx-gui Apps](./spx-gui-apps.md)
- [Account Local Debugging](./account-local-debugging.md)
- [Maintaining the Default Project](../../skills/xbuilder-default-project/SKILL.md)

### AI Interaction

- [AI Interaction Sentry](./ai-interaction-sentry.md)
- [AI Interaction Sentry Implementation](./ai-interaction-sentry-implementation.md)
Loading