feat(browser): B3 截图→上下文闭环 + B4 面板声明页(Y4)

- B3:截图落盘后**自动**并入本轮视图(能收图 ⇒ 真 image 部件挂 user 消息;收不了 ⇒ vision/OCR 文字;
  大图先经 image-crop 裁切;取走即消费、每会话 ≤4 张、TTL 10min、跨会话隔离)
- B4:browser 面板**声明**(配置表单 + 运行状态/指纹自检/最近截图三张只读表 + 拍一张/重新自检)
  + 六个动作走 PLG-2 的通用动作端点(**不开新路由**)+ 保存事务(不可用则回滚 409)+ 热重载
- 读面**不启动浏览器**(计数型假 dialer 证明 0 次拨号);行数据不含路径/URL/内容
- 3 个变异(短路主路/行与列 key 脱钩/保存不回滚)
⚠️ **两行接线未落**(不在我文件面):wire_runtime.go 的 TransformContext + wire_frontend.go 的
   wireBrowserPanel —— 落地前 B3/B4 只是「组件就绪」,跑着的后端里不存在(Y4 已写盯梢用例)
This commit is contained in:
AskaEth committed 2026-10-06 21:48:50 +08:00
1 parent 4213b268fe
commit e0a6bca501
18 files changed
+3698 -72

No files matched your search

+31 -7
View File
@@ -68,16 +68,29 @@ func (r *MessageResolver) Resolve(ctx context.Context, msg model.Message) (model
return model.Message{Role: msg.Role, Content: out}, nil
}
// resolveImage 选出一条路由:要么保留图像(KindMain),
// 要么经 vision/OCR 工具把它折叠为文字描述。
func (r *MessageResolver) resolveImage(ctx context.Context, imageURL string) (model.Content, error) {
// describePrompt 是走 vision 工具路由时给视觉模型的默认指令。它是**命名常量**
// 而不是散在两处的字面量:`resolveImage` 与 `ShotAttacher` 必须用同一句,
// 否则同一种图会因为「谁发的」而得到两种提问方式(结果不可比)。
const describePrompt = "Describe this image in detail."
// decision 是路由判定的唯一读取点:把「主模型能力 → Router」这一步收在一处,
// 使 resolveImage(用户附件)与 ShotAttacher(截图附件)不可能各自算一次而分叉。
func (r *MessageResolver) decision() Decision {
if r == nil || r.router == nil {
return Decision{Kind: KindUnsupported, Reason: "no vision router configured"}
}
target := ModelInfo{}
if r.info != nil {
target = r.info()
}
decision := r.router.Route(target, RouteRequest{})
switch decision.Kind {
case KindMain:
return r.router.Route(target, RouteRequest{})
}
// resolveImage 选出一条路由:要么保留图像(KindMain),
// 要么经 vision/OCR 工具把它折叠为文字描述。
func (r *MessageResolver) resolveImage(ctx context.Context, imageURL string) (model.Content, error) {
decision := r.decision()
if decision.Kind == KindMain {
// 支持视觉的主模型直接看到这张图;但 URL 与本地路径必须先由服务端
// 取回并内联为 data: URI——云 provider 取不到自部署的局域网地址
// (如 http://192.168.x.x:10942/api/v1/files/…),Anthropic 协议更是
@@ -90,10 +103,21 @@ func (r *MessageResolver) resolveImage(ctx context.Context, imageURL string) (mo
return model.Content{}, err
}
return model.Content{Type: model.ContentTypeImage, ImageURL: src}, nil
}
return r.resolveToolRoute(ctx, imageURL, describePrompt, decision)
}
// resolveToolRoute 执行工具形态的那两条路由(vision / OCR):图像不进主模型
// 上下文,只把工具产出的文字折叠成一个文本部件。没有工具可用时如实报
// ErrUnsupportedSource(**绝不**悄悄把图像留给一个读不了图的主模型)。
func (r *MessageResolver) resolveToolRoute(
ctx context.Context, imageURL, prompt string, decision Decision,
) (model.Content, error) {
switch decision.Kind {
case KindVisionTool:
return r.invokeTool(ctx, decision.Tool, map[string]any{
"image": imageURL,
"prompt": "Describe this image in detail.",
"prompt": prompt,
}, "description")
case KindOCRTool:
return r.invokeTool(ctx, decision.Tool, map[string]any{