# REQ — pi 薄插件（extensions/pi）实现要求：OCGo Gateway 配合版

> 位置：`ocgo-gateway` 仓库内 `extensions/pi-ocgw/`（与网关主体同分支）
>
> 背景：`ocgo-gateway`（独立常驻网关，端口 8130）已把**多 key 池、配额感知、用量统计、
> 对话级绑定、Agent 级 gateway-key、总控 Web 面板**全部下沉到网关。
> 本目录 `extensions/pi/` 是配套的 Pi 极简薄插件，只保留
> 宿主侧无法下沉到网关的薄能力。

---

## 0. 目标与原则

**单一职责**：mini 插件不再持有 key 池 / 配额 / 用量 / Web 面板，这些全部由网关（`ocgo-gateway`，端口 8130）负责。
源码参考旧仓库 `pi-ocgo` 同名模块拷入改造（`developerCompat` / `cacheOptimizer` 等）。

插件保留的能力（宿主侧必须留下的事 + 薄壳命令）：

1. **指向网关**（provider baseUrl → 网关 `/v1`）。
2. **注入身份**（`Authorization: Bearer <gateway-key>`）。
3. **注入对话标识**（`x-ocgo-conversation: <会话ID>`，网关据此做对话级绑定）。
4. **developer 兼容改写**（`role:developer`→`system`，修 400 —— 依赖 `before_provider_request` 宿主钩子，网关透传做不了）。
5. **缓存前缀稳定**（删 reasoning / 删时间戳 / 排序 tools —— 依赖 `context` / `before_agent_start` 宿主钩子，网关做不了；计费命门，缓存命中≈1/30 全价）。
6. **状态/统计输出**（TUI widget 进度条 / footer 摘要 / 定时推送 / 命令输出——呈现层在宿主，数据走网关 API）。
7. **上下文交接提醒 / handoff**（**保留，成熟功能**：双水位阈值判断 + 交接文档生成 + 换 K/新对话衔接；比 pi 自带压缩好得多——`/compact` 前缀分叉全价 miss，handoff 在正常对话里生成交接文档、前缀延续缓存命中≈1/30，换 K 成本从 X 降到 S。依赖 message_end 用量 + 宿主会话管理）。
8. **薄命令**（`/ocgw` 走网关控制面 API，本地不存任何上游 key）。

**克制铁律**：转发链路 / 配额 / 用量 / 面板一律不碰；宿主侧无意义的逻辑一律删。让位给网关，插件做「壳 + 宿主侧必要的薄能力」。

> ⚠️ 与 DESIGN 阶段 4 结论一致：developer 兼容改写、缓存前缀稳定、上下文交接提醒依赖宿主内部上下文，
> **无法收敛进网关**（已核实网关 `upstream.ts` 纯透传，不内置这些）。mini 版取舍见 §6：
> 保留 4、5；6 保留（数据源换网关）；7 薄命令；handoff 砍（与网关无关）。

---
## 1. 配置：插件只存「网关地址 + gateway-key」

> 约定：**没有 "defaultKey" 配置**。未绑定对话默认走上游 **active key**——这是网关规则
> （`decideKey` 兜底策略 `source: "pool"`），不是插件本地配置项。插件本地配置只有两样：连哪台网关 + 我是谁。

```jsonc
// ~/.pi/agent/ocgo-gateway.json（权限 600，仅此一个本地配置文件）
{
  "gatewayBase": "http://127.0.0.1:8130/v1",   // 网关 OpenAI 兼容地址（Agent 级）
  "gatewayKey": "ocgo_VoQqAmjqiulPNbzj"        // 本插件的 gateway-key，身份（Agent 级）
}
```

- `gatewayBase` 缺省 `http://127.0.0.1:8130/v1`（可用环境变量 `OCGO_GATEWAY_URL` 覆盖）。
- `gatewayKey` 由网管在网关面板签发后填入；缺省时插件提示「未配置 gateway-key」，请求前必须配置。
- **不存任何上游 sk- key，也不存默认 key**——未绑定对话默认走上游 active key（网关规则，无需配置）。

### 1.1 两个层级：Agent 级（全局）vs 对话级（每会话）

| 层级 | 内容 | 作用域 | 谁在管 |
|------|------|--------|--------|
| **Agent 级（全局）** | base URL + gateway-key | 整个 Pi 一份，所有对话共用，与对话无关 | 插件本地配置文件（一次） |
| **对话级（每会话）** | 绑定到哪个上游 K | 每条对话自己的选择，同一条对话内可变 | 网关 binding 表（插件只发指令） |

### 1.2 指令拆分（Agent 级 / 对话级 / 查询）

**Agent 级：一次性设置 Pi 的「身份 + 连地址」**（写本地配置）

| 指令 | 作用 | 去向 |
|------|------|------|
| `/ocgw setup <url> <key>` | 一次设置网关地址 + gateway-key | 本地配置 |
| `/ocgw url <url>` | 只改网关 base URL | 本地配置 |
| `/ocgw key <key>` | 只改 gateway-key | 本地配置 |
| `/ocgw whoami` | 查看当前身份与生效地址 | 本地配置 + 网关校验 |

**对话级：每条对话选择走哪条上游额度**（写网关，插件不存）

| 指令 | 作用 | 网关 API |
|------|------|----------|
| `/ocgw bind [<key名>]` | 当前会话绑定；**缺省 key 名 = 绑定当前 active key**（网关规则，已实现） | `POST /admin/bind` |
| `/ocgw unbind` | 当前会话解绑，回落默认（active key 或已设的 default） | `POST /admin/unbind` |
| `/ocgw default <key名>` | 设置 Pi 级默认上游 K（可选；不设则未绑定对话走 active key） | `POST /admin/set-default` |

**查询：当前 key 状态 + 统计输出（只读，走网关）**

| 指令 | 作用 | 网关 API |
|------|------|----------|
| `/ocgw status` | 看 key 池状态 + 当前绑定（哪个 key 激活 / 本会话绑定到谁 / 默认是谁） | `GET /admin/list` + `GET /admin/quota` |
| `/ocgw keys` | 列出全部上游 key 信息（名称/前缀/状态/激活/配额/重置时间） | `GET /admin/list` + `GET /admin/quota` |
| `/ocgw quota [key名]` | 查指定 key（缺省=当前生效 key）的 R/W/M 配额 + 重置时间 | `GET /admin/quota` |
| `/ocgw cost [范围]` | 查本 agent 用量/费用（today/3d/7d/all，缺省 all；按 key/对话汇总） | `GET /admin/stats` |

> **核心：地址+身份 = Agent 级、全局、写一次；绑定 = 对话级、可变、归网关；查询/统计只读网关。**
> 本地 config 只有两个身份字段；绑定关系不落本地——只在网关，插件每次请求带 `x-ocgo-conversation` 让网关自己解析。

---
## 2. Provider：指向网关（OpenAI 兼容）

`ensureOpencodeProvider` 改为注册一个 provider，`baseUrl` 指向网关：

```ts
pi.registerProvider("pi-ocgw", {
  name: "OCGo Gateway",
  baseUrl: "http://127.0.0.1:8130/v1",   // 读配置 gatewayBase
  apiKey: "<gatewayKey>",                 // 占位，before_provider_headers 再真正注入
  authHeader: true,
  api: "openai-completions",
  models: [ ...deepseek-v4-flash/pro、glm-5.1、kimi-k2.6、qwen3.6-plus... ],
});
```

- provider id 固定为 `pi-ocgw`（与插件名一致）（或复用 `opencode-go` 名字以免破坏现有 `/model` 习惯，见 §6）。
- models 清单：网关 `/v1/models` 已有静态清单，插件可硬编码同款，或启动时拉取（见 §7 待办）。
- **不再需要 `TARGET_PROVIDERS` 多 provider 集合** —— 只认这一个网关 provider。

---

## 3. 请求注入（before_provider_headers）

原「能力 B」逻辑（选 key + 轮换 + 粘合）**整体删除**。替换为极简注入：

```ts
pi.on("before_provider_headers", (event, ctx) => {
  if (ctx.model?.provider !== "ocgo-gateway") return;
  const gw = loadGatewayConfig();
  if (!gw.gatewayKey) return;
  event.headers["Authorization"] = `Bearer ${gw.gatewayKey}`;
  // 关键：对话级绑定标识（DESIGN §2.1，网关据此解析对话 → 上游 K）
  const sid = currentSessionId(ctx);
  if (sid) event.headers["x-ocgo-conversation"] = sid;
});
```

- 不选 key、不轮换、不粘合、不写任何本地状态 —— 选 key/轮换/配额全部由网关按
  `gateway-key + x-ocgo-conversation` 决定。
- `after_provider_response` 的 429 轮换逻辑删除（网关已处理，且 429 应透传提示用户）。

---

## 3.1 Caddy 层认证对齐（已定案：方案 B，2026-08-19 落地验证）

现网部署链路（服务器资源文档 §ocgo.zlxy.sd.cn）：

```
Agent → https://ocgo.zlxy.sd.cn
        → Caddy（容器 zlxy-website）:
            /admin /admin/ /admin/index.html → basic_auth(robin)   （面板，人看要密码）
            /admin/* 其余端点 + /api/* 别名 → 放行（网关 gateway-key 认证）
            /v1/*                            → 放行（网关 gateway-key 认证）
        → ocgo-gateway:8130（zlxysdcn_default 网络，容器名直连）
        → 上游 https://opencode.ai/zen/go/v1
```

**认证职责分工（已落地并实测通过）：**

| 请求路径 | Caddy | 网关 | 谁访问 |
|----------|-------|------|--------|
| `/admin` `/admin/`（HTML 面板） | basic_auth(robin) | 宽松（面板 JS 无 gateway-key） | 管理员浏览器 |
| `/admin/*` 其余 API（bind/list/quota…） | 放行 | 宽松（靠 Caddy basic_auth 保护面板场景） | 管理员经面板调用 |
| `/api/*`（控制面 API 别名） | 放行 | **强制 gateway-key（插件专用）** | **插件命令** |
| `/v1/*`（对话请求） | 放行 | 强制 gateway-key | 插件对话请求 |

**网关侧已实现（src/gateway/server.ts）：**

```ts
// 1) 入口：/api/* → /admin/* 别名
const rawUrl = (req.url ?? "/").split("?")[0];
let url = rawUrl;
if (url.startsWith("/api/")) url = "/admin/" + url.slice(5);

// 2) 控制面鉴权：/api/* 插件专用路径强制 gateway-key（rawUrl 判断，别用归一后的 url）
if (rawUrl.startsWith("/api/")) {
  const auth = authenticate(req.headers.authorization);
  if (!auth.authenticated) return sendJson(..., 401);
  req._gwAgentId = auth.agentId;   // 注入认证 agentId
}

// 3) bind/unbind/set-default：agent 优先取 req._gwAgentId，body.agent 可省；bind 缺省 keyName = active key
const agentId = req._gwAgentId ?? String(body.agent ?? "");
```

> ⚠️ **服务器需保持 gateway-keys.json（网关认证的 agent 表）**：
> `/root/ocgo-gateway/data/gateway-keys.json`。此文件缺失时网关宽松放行=控制面无认证。
> 同步命令：`scp ~/.ocgo-gateway/gateway-keys.json root@118.190.206.142:/root/ocgo-gateway/data/` + `docker restart ocgo-gateway`

---
## 4. 命令：/ocgw 全走网关 API

重写命令为 `/ocgw`，子命令薄封装网关 HTTP 控制面 API：

| 子命令 | 作用 | 网关 API |
|--------|------|----------|
| `/ocgw setup <url> <key>` | 设网关地址 + gateway-key（写本地配置） | — |
| `/ocgw url <url>` | 只改网关地址 | — |
| `/ocgw key <key>` | 只改 gateway-key | — |
| `/ocgw whoami` | 查当前身份/地址 | — |
| `/ocgw status` | 看 key 池状态 + 当前绑定/活跃/默认 | `GET /admin/list` + `GET /admin/quota` |
| `/ocgw keys` | 列出全部上游 key 信息 | `GET /admin/list` + `GET /admin/quota` |
| `/ocgw bind [<key名>]` | 当前会话绑定；缺省 = 绑当前 active key | `POST /admin/bind`（带 conversation=当前会话ID） |
| `/ocgw unbind` | 解绑回默认（active/default） | `POST /admin/unbind` |
| `/ocgw default <key名>` | 设 Pi 级默认上游 K（可选） | `POST /admin/set-default` |
| `/ocgw quota [key名]` | 查配额（缺省=当前生效 key） | `GET /admin/quota` |
| `/ocgw cost [范围]` | 查本 agent 用量/费用（today/3d/7d/all） | `GET /admin/stats` |
| `/ocgw use <n>` | 切换活跃 key（网管） | `POST /admin/key/use` |
| `/ocgw add <名> <key>` | 加上游 key（网管） | `POST /admin/key/add` |
| `/ocgw rm <名>` | 删上游 key（网管） | `POST /admin/key/remove` |
| `/ocgw handoff` | 生成交接文档并新建会话继续（确认后执行） | 本地 docs/handoff + 网关 bind/unbind |
| `/ocgw resume` | 恢复旧的交接文档继续 | 本地 docs/handoff |
| `/ocgw expire/revive/pause/unpause` | key 状态管理（网管） | 网关对应 admin 接口 |
| `/ocgw notify <秒>` | 定时推送限额/统计（数据走网关，默认开） | `GET /admin/quota` + `/admin/stats` |

- **全部带 `Authorization: Bearer <gatewayKey>`** 调网关控制面。
- `bind` 的 conversation = 当前 pi 会话 ID（与 §3 注入的 header 一致，保证同一标识）。
- **删除**：`web`（面板归网关）、`handoff/resume`（如不保留交接则删）、
  `cache`、`cooldown`、`watchdog`（这些宿主侧能力按 §6 取舍）；`notify` 保留（改走网关数据）。

---
## 4.5 统计/配额输出（重要：不是删，而是改数据源）

原插件有三类面向用户的输出，mini 版**全部保留，呈现层不变**，只是数据源从本地改为网关 API：

| 输出类型 | 展示内容 | 原数据源 | mini 数据源 |
|----------|----------|----------|-------------|
| **TUI widget 面板** | 每个 key 的限额进度条（R/W/M 三档 + 重置时间） | 插件直打上游 /usage + 本地 cache | 网关 `GET /admin/quota`（网关已 60s 节流缓存） |
| **footer 摘要** | 激活 key 限额摘要一行 | 同上 | 同上 |
| **定时推送** | `★ key: R% W% M% →(重置)` 每行 + `key 总统计: N 次·输入·输出·缓存命中` + `⏱ 统计发送时间` | 插件本地 usageStore | 网关 `GET /admin/quota` + `GET /admin/stats`（按 key 汇总，含 cached/cost） |
| **/ocgw 命令输出** | status/quota/cost 的 notify 展示 | 本地 store | 网关 API |

> 网关 `GET /admin/stats` 返回 `keys[]`，每项 `{keyPrefix, records, prompt, completion, cached, totalTokens, cost, lastTs}`
> —— 换算成原插件那种 `k3 总统计: 1,352 次 · 输入 7,561,547 · 输出 527,671 · 缓存命中 323,647,872 (97.72%)` 完全可行。
> 网关 `GET /admin/quota` 返回 `keys[]` 每项 windows `{name, status, percent, resetsAt}` + `text`（网关已格式化）——
> 直接渲染成 `R0% · W62% · M31% →(09/17 04:45)` 进度条/文本无差异。
>
> **注意过滤维度**：原插件的「总统计」按当前激活 key（keyPrefix）过滤；mini 版建议按
> **当前对话绑定+当前 agent** 维度或「本 agent 全部」维度展示（见 §7 待确认），避免多会话串台下误导。

---

## 5. 删除清单（不再需要的模块/文件）

| 现有内容 | 处理 | 理由 |
|----------|------|------|
| `src/core/config.ts` 的 key 池/冷却/封禁/粘合 | 删 | 归网关 |
| `src/core/keyRouter.ts`（pickKeyForSession/轮换） | 删 | 归网关 |
| `src/core/usage.ts`（fetchUsage 打上游 usage） | **改** | 删直打上游逻辑，改为调网关 `/admin/quota` |
| `src/core/usageStore.ts`（本地落盘用量） | **改** | 不再本地落盘，统计改走网关 `/admin/stats`；但其**格式化/汇总函数可保留复用** |
| `src/core/handoff.ts` + `sessionWorkspace.ts`（交接） | **保留** | 交接保留（§6-3），依赖宿主会话管理，走网关 bind/unbind 衔接 |
| `src/core/pricing.ts` | 删（供应商价格表） | 计价归网关；若插件要展示费用估算可保留（待确认） |
| `src/core/webui.ts` + `portfile.ts` + `src/serve.ts` + `ocgo-web.sh` | 删 | 面板归网关 |
| `pi/index.ts` 的 429 轮换 / session 粘合 | 删 | 归网关 |
| `pi/index.ts` 的配额推送 / widget / footer / 统计输出 | **保留，改数据源** | 呈现层不动，数据改调网关（§4.5） |

**新增**：
- `src/core/gatewayClient.ts` —— 纯 Node 的网关 HTTP 控制面客户端（零 pi 依赖，可被 dsh 复用）。
- `src/core/config.ts` 改写为极简 `{gatewayBase, gatewayKey}`。

---

## 6. 宿主侧能力保留（已定案）

网关 DESIGN 明确「薄插件并不薄」：以下依赖宿主上下文，**无法下沉到网关**。mini 版取舍已定：

> ✅ **已核实（2026-08）**：网关 `ocgo-gateway` 侧**并未**内置 developer 兼容改写、也未内置 DeepSeek 缓存前缀优化——
> `src/gateway/upstream.ts` 是纯透传（只换 Authorization），`src/core/` 明确不导出 developerCompat/cacheOptimizer。
> 这些能力依赖 pi 宿主钩子（`before_provider_request` / `context` / `before_agent_start` / `message_end`），**只有插件能做**。

**全部保留（4 项宿主侧能力）：**

1. **developer 兼容改写** — 保留（修 400）。
2. **缓存前缀稳定** — 保留精简版（省 ~30 倍费），用默认白名单（`deepseek/kimi/glm/mimo/minimax/qwen`），不提供 `/ocgw cache` 命令。
3. **上下文交接提醒 / handoff** — **保留（成熟功能，比 pi 压缩好）**：
   - 双水位阈值（上下文 ≥80% 主动提醒、≥95% 紧急提醒 + 配额紧张触发）
   - 交接文档模板（目标/约束/进度/决策/下一步/文件清单）+
     注入 pi 跟踪的 read/modified 文件清单，≤30K 精炼
   - 执行：确认 → 同会话生成交接文档 → 落盘 `docs/handoff/` → 新建会话注入交接文档继续
   - 命令：`/ocgw handoff`（生成+新建）/ `/ocgw resume`（继续旧交接）
4. **状态/统计输出** — 保留（数据源换网关 API，呈现层不变）。

> 唯一与网关相关的影响：换 K/新建对话时，交接前的绑定（bound conversation）属网关 binding 表，交接后新建对话解绑旧会话、绑新 K——插件调用网关 `/admin/bind` / `/admin/unbind` 完成（与 §1.2 对话级指令一致）。

---
## 7. 待确认 / 后续待办

- [ ] Provider id 定 `ocgo-gateway` 还是沿用 `opencode-go`（影响用户 `/model` 习惯与现有迁移）。
- [x] 插件 package name：已定 `pi-ocgw`（文件夹 `extensions/pi-ocgw`）
- [ ] 模型清单：硬编码同款，还是启动时 `GET /v1/models` 从网关拉取（网关是唯一模型真源）。
- [ ] 统计输出的展示维度：按当前绑定 key / 本 agent 全部 / 按对话（§4.5 注意项）——实现时定。
- [ ] 测试适配：现有 `test/*.test.ts` 大量测 keyRouter/usageStore，需重写为测 gatewayClient + 薄逻辑。
- [ ] README / README-zh / DEVELOPMENT / ARCHITECTURE 全量改写成「极简网关配合版」。
