# Phase 7 剩余项与已知生产缺陷追踪 7.7 DSH 插件已完成(见 `docs/PLUGIN-GUIDE.md` 与 PLAN.md §7.7)。 ## 无法立即推进(缺基础设施) ### 7.8 跨主机 Agent 发现 - Gateway + Registry 拆分为独立服务 - etcd / Consul 服务注册与发现 - Agent 跨主机路由 ## 可以立即推进的生产缺陷 ### P0 — SSE Last-Event-ID 补投 **根因**:EventSource 断线重连时自带 Last-Event-ID 头,但服务端直接忽略了—— 所有断线期间的邮件通知都丢失。用户刷新页面也会错过已推的事件。 **影响**:重连后永远看不到断线期间收到的邮件(除非手动刷新)。 **修法**:服务端维护一个有界循环缓冲区(ring buffer),每次 Broadcast 同时写入, SSE 连接的 handler 在首次连接时从缓冲区头部开始(客户端传了 Last-Event-ID 就从那里), 没有则从头(只带最近 N 条)。缓冲区大小设 500,内存 < 2MB。 ### P0 — 连接状态指示器 **根因**:SSE 断线后前端无任何可见反馈——用户以为系统正常,实际通知已停。 **影响**:实时性是 Agent 协作的核心体验,断线无提示会让人以为「Agent 没在动」。 **修法**:header 旁加一个连接状态点(绿/黄/红),SSE 的 onopen/onerror 事件驱动。 ### P1 — 登录限速跨进程问题 ✅ **根因**:LoginLimiter 是进程内内存计数器,多实例部署时每个实例独立计数。 **修法**:改为 DB 事务(rate_limits 表 + IMMEDIATE 事务),多实例共享同一份计数。 ### P1 — 新建会话限速同理 ✅ **根因**:sessionRateLimiter 也是进程内计数器。 **修法**:同上,sessionrate.go 重写为调用 RateLimitCheckAndRecord。 ### P2 — 组件级测试 **现状**:前端无任何组件测试,前端回归只靠 lint 与构建。 **范围**:关键组件(AddressInput 补全、PermissionPanel 决策、WorkCard 预算渲染)。 ### P2 — 深色主题 **现状**:只有浅色主题,深夜使用刺眼。 **范围**:tailwind dark: 前缀覆盖主要组件。 ### P1 — 每平台可用模型范围 ✅ **需求**:配置页面为每个 Agent 平台划定「邮箱调用场景下可用的模型范围」, 端侧插件按范围**逐个降级尝试**,全部失败时把失败原因封装成邮件回复。 选择而非手打模型名 —— 平台上报目录,管理员勾选。 **已完成**: - `agent_model_catalog`(平台上报的目录)+ `agent_allowed_models`(管理员的选择) 两张表,两份 schema - `repo/models_scope.go`:`ReplaceModelCatalog` / `ListModelCatalog` / `ListAllowedModels` / `SetAllowedModels` **为什么分两张表**:模型会从平台目录里消失(换了 provider 配置、上游临时下线), 整行删掉会连带把管理员的选择也删了,模型回来还得重配一遍。分开存之后 「选了什么」是持久的,目录只决定「这一项现在是否可用」。 **已完成(全部)**: - [x] handler + 路由:`GET/PUT /admin/agents/{name}/models`、`GET /agent/models/allowed` - [x] **目录上报走心跳**而不是另设端点:模型清单会在运行中变, 心跳本来就是 30 秒一次的现成通道;另设一个 POST 等于给「目录是谁写的」 留两个答案 - [x] 心跳响应回传 `allowed_models`:管理员改了范围后最多一个周期生效,不必重启 - [x] `lib/model-scope.js`:目录整理(两平台)、`modelAttemptOrder`、`renderFailureReport` - [x] 插件按 rank 逐个尝试,全部失败发一封说明原因的邮件(走免配额通道) - [x] 前端 `ModelScopePanel`:勾选 + 上下移调序 + stale 标记 **最难的一点**(两个平台都踩了):**模型失败不是同步抛出的**。 `promptAsync()` 立即返回、`ctx.agents.create()` 不校验模型,只包 try/catch 第二个模型永远不会被试到。要等异步结论: - opencode → `session.error` 事件 - DSH → `turn/end` 的 `reason.kind === 'error'` DSH 还有个陷阱:`assistant/chunk` 的 `finish` 子类型也带错误, 把任意 chunk 当成功会让无效 provider 判成走通(实测踩过)。