feat(browser): 补上 B3/B4 的两行接线(Z2)—— 面板页与截图进上下文真生效
- wire_runtime.go 加 TransformContext: a.browserShotTransform(visionResolver)(只改本轮所见) - wire_frontend.go 在 declareBuiltinPanels 之后接 wireBrowserPanel(带 a.cfg != nil 守卫, 否则 6 处既有夹具 panic;生产路径 cfg 恒非 nil ⇒ 行为与字面写法一致) - 3 条盯梢用例(1 条 Skip→转正 + 2 条新增),2 变异打红 - 真机(隔离实例):panels 出现 browser/settings、三个只读动作真回执、**真回合内联 image 部件 61,410 字符** ⇒ design/28 判据 2 在真产物里成立
This commit is contained in:
1 parent
93fc24cade
commit
8e2441c183
3 files changed
+131
-15
No files matched your search
@@ -14,10 +14,10 @@ package main
|
||||
// - 「图 → 内容部件」的路由与裁切由 `internal/runtime/multimodal/vision/shot_test.go` 覆盖;
|
||||
// - 本文件把两者接起来,并且**断言模型侧确实拿到 image 部件**(判据 2 的原文要求)。
|
||||
//
|
||||
// ⚠️ **接线状态(如实)**:本文件把 `a.browserShotTransform(...)` 装进 `loop.Options`
|
||||
// 来验它;生产是否装上由 `wire_runtime.go` 的一行决定(本片无权改那个文件,见
|
||||
// `wire_browser_context.go` 的接线状态注释)。`TestWireRuntime_WiresBrowserShotTransform`
|
||||
// 会盯着那一行:装上之前它 Skip 并写明要加什么,装上之后它就是正向断言。
|
||||
// 生产侧的**两条挂点**都由本文件盯住(它们删掉之后都没有任何运行期症状):
|
||||
// `TestWireRuntime_WiresBrowserShotTransform` 钉 `wire_runtime.go` 的 `TransformContext`;
|
||||
// `TestWireFrontend_WiresBrowserPanel` 钉 `wire_frontend.go` 的 `wireBrowserPanel` 调用
|
||||
// (`TestWirePanelHub_DeclaresBrowserPanel` 是它的**行为侧**配偶:真装配一遍再读回目录)。
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -30,7 +30,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||
browsercap "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/browser"
|
||||
pluginmgr "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/plugin"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
@@ -113,7 +116,8 @@ func newB3Fixture(t *testing.T, sighted bool) *b3Fixture {
|
||||
shot: shotPath,
|
||||
loop: loop.New(loop.Options{
|
||||
Agent: ag,
|
||||
// ← 生产接线就是这一行(见 wire_runtime.go 的接线状态注释)。
|
||||
// ← 与生产接线逐字同一行(wire_runtime.go 的 loop.Options.TransformContext),
|
||||
// 由 TestWireRuntime_WiresBrowserShotTransform 源码级盯住。
|
||||
TransformContext: a.browserShotTransform(resolver),
|
||||
OnStream: func(model.StreamEvent) {},
|
||||
}),
|
||||
@@ -194,21 +198,95 @@ func TestBrowserShotClosure_ShotIsConsumedOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWireRuntime_WiresBrowserShotTransform 盯住生产接线那一行。
|
||||
// TestWireRuntime_WiresBrowserShotTransform 盯住生产接线那一行(B3 的生产挂点)。
|
||||
//
|
||||
// 装上之前**Skip 并写明要加什么**(不是失败:本片(Y4)的文件面不含 wire_runtime.go,
|
||||
// 那一行由主会话落地)。装上之后本用例就是正向断言 —— 它拦的是「以后有人把那行删了」,
|
||||
// 因为删掉之后**没有任何运行期症状**:编译照过、回合照跑,只是截图不再进上下文。
|
||||
// 它拦的是「以后有人把那行删了」:删掉之后**没有任何运行期症状**——编译照过、回合照跑,
|
||||
// 只是截图不再进上下文。因此断言的是**一整行字段赋值**(行首到行尾),被注释掉的
|
||||
// (`// TransformContext: …`)或写在别处的同名字符串都不会让它通过。
|
||||
func TestWireRuntime_WiresBrowserShotTransform(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := readSource(t, "wire_runtime.go")
|
||||
if !strings.Contains(src, "browserShotTransform(") {
|
||||
t.Skip("接线未应用:请在 wire_runtime.go 的 loop.New(loop.Options{…}) 里加 " +
|
||||
"`TransformContext: a.browserShotTransform(visionResolver),`" +
|
||||
"(B3 闭环的生产挂点;本片无权改该文件)")
|
||||
|
||||
const assignment = "TransformContext: a.browserShotTransform(visionResolver),"
|
||||
if !hasNormalizedCodeLine(src, assignment) {
|
||||
t.Fatalf("wire_runtime.go 的 loop.Options 里找不到 %q —— B3 截图闭环的生产挂点缺失:"+
|
||||
"截图照落盘、回合照跑,只是**一张图都不会进上下文**", assignment)
|
||||
}
|
||||
if !strings.Contains(src, "TransformContext:") {
|
||||
t.Fatal("wire_runtime.go 里出现了 browserShotTransform,但没挂在 TransformContext 上")
|
||||
// 钩子捕获的就是 `visionResolver`,因此它必须在 loop.New 之前建好;顺序反过来装上的
|
||||
// 就是一个 nil 解析器(ShotAttacher 为 nil),截图静默不送——同样是**零症状**的坏法。
|
||||
idxResolver := strings.Index(src, "visionResolver := vision.NewMessageResolver(")
|
||||
idxLoop := strings.Index(src, "a.loop = loop.New(loop.Options{")
|
||||
if idxResolver < 0 || idxLoop < 0 || idxResolver > idxLoop {
|
||||
t.Fatalf("visionResolver 与 loop.New 的先后不对(偏移 %d vs %d):钩子必须在解析器**之后**装配",
|
||||
idxResolver, idxLoop)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWireFrontend_WiresBrowserPanel 盯住 B4 的生产挂点:`wirePanelHub` 必须在
|
||||
// `declareBuiltinPanels` 之后调用 `wireBrowserPanel`。
|
||||
//
|
||||
// 与上面那条同族——删掉之后**没有任何编译期或运行期症状**:只是面板目录里没有 browser 页、
|
||||
// 六个动作全回 501(`GET /api/v1/admin/panels` 少一条、点击全部 501),而整树测试照绿。
|
||||
// 断言同样锚定**一整行调用**,注释掉那一行不会通过。
|
||||
func TestWireFrontend_WiresBrowserPanel(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := readSource(t, "wire_frontend.go")
|
||||
|
||||
const call = "wireBrowserPanel(hub, a, a.log)"
|
||||
if !hasNormalizedCodeLine(src, call) {
|
||||
t.Fatalf("wire_frontend.go 的 wirePanelHub 里找不到 %q —— B4 的 browser 面板声明"+
|
||||
"与六个动作都进不了目录(面板无 browser 页、动作全 501)", call)
|
||||
}
|
||||
// 位置:声明在前、登记在后是 PLG-2 的硬要求(登记期会核对有没有对应声明)。这里钉住
|
||||
// 它**在 wirePanelHub 内**且**在 declareBuiltinPanels 之后**,防止被挪到别处或提前。
|
||||
idxDeclare := strings.Index(src, "declareBuiltinPanels(hub, log)")
|
||||
idxBrowser := strings.Index(src, call)
|
||||
if idxDeclare < 0 || idxBrowser < idxDeclare {
|
||||
t.Fatalf("wireBrowserPanel 的调用位置不对(declareBuiltinPanels 偏移 %d,调用偏移 %d):"+
|
||||
"它必须在声明之后(wirePanelHub 内部)", idxDeclare, idxBrowser)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWirePanelHub_DeclaresBrowserPanel 是上面那条源码级断言的**行为侧配偶**:真的走一遍
|
||||
// `wirePanelHub` 装配,再从面板中枢里读回目录与动作白名单 —— 声明在、六个动作也登记上,
|
||||
// 才算 B4 的接线成立。
|
||||
//
|
||||
// 两条合起来才够:源码级那条拦「那一行被删掉」,这条拦「那一行还在、但调用链断了」
|
||||
// (wireBrowserPanel 被改成早返回、hub 传错、或声明/登记顺序被调反 ⇒ 只会得到一串 WARN)。
|
||||
// 这台机器上**没有**浏览器也没关系:不可用时面板仍要如实声明(`available=false`),
|
||||
// 这正是 design/09 §5.2.1 的「该面板不注册 + 显式报错」之外的正常档。
|
||||
func TestWirePanelHub_DeclaresBrowserPanel(t *testing.T) {
|
||||
t.Parallel()
|
||||
mgr := pluginmgr.NewManager(registry.New(), filepath.Join(t.TempDir(), "plugins.json"), testLog())
|
||||
defer mgr.Close(context.Background())
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
|
||||
wirePanelHub(&app{cfg: &config.Config{DataDir: t.TempDir()}, plugins: mgr, bus: b, log: testLog()})
|
||||
hub := mgr.Panels()
|
||||
if hub == nil {
|
||||
t.Fatal("wirePanelHub 未把面板中枢接到插件管理器上")
|
||||
}
|
||||
entries, err := hub.List(auth.RoleOwner)
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, e := range entries {
|
||||
if e.PluginID == browsertools.PanelPluginID && e.Panel.ID == browsertools.PanelIDSettings {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("面板目录里没有 browser/settings(目录=%+v):B4 的挂点没生效", entries)
|
||||
}
|
||||
for _, action := range []string{
|
||||
browsertools.ActionState, browsertools.ActionFingerprint, browsertools.ActionShots,
|
||||
browsertools.ActionShoot, browsertools.ActionCheck, browsertools.ActionSaveConfig,
|
||||
} {
|
||||
if _, ok := hub.Action(browsertools.PanelPluginID, action); !ok {
|
||||
t.Errorf("动作 %q 没进白名单(声明有、没人执行 ⇒ 面板点下去只会回 501)", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +294,26 @@ func TestWireRuntime_WiresBrowserShotTransform(t *testing.T) {
|
||||
* 断言助手
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
// hasNormalizedCodeLine 报告源码里是否存在这样**一行代码**:去掉行首空白、把连续的空白
|
||||
// 折叠成一个空格之后,与 want 逐字相等。被 `//` 注释掉的行一律不算。
|
||||
//
|
||||
// 为什么要折叠空白:字段赋值在 gofmt 里按块对齐(`TransformContext:` 与
|
||||
// `DocumentMaxInjectChars:` 同一组时中间是 7 个空格,格式而非语义),逐字节比对整行会
|
||||
// 在**一次无关的字段增删触发重新对齐**时误红;而折叠后逐行相等仍然能挡住
|
||||
// 「注释掉那一行」与「别处的同名字符串」这两种假绿。
|
||||
func hasNormalizedCodeLine(src, want string) bool {
|
||||
for _, line := range strings.Split(src, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
if strings.Join(strings.Fields(line), " ") == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findImagePart 返回请求里的第一个 image 部件。
|
||||
func findImagePart(msgs []model.Message) (model.Content, bool) {
|
||||
for _, m := range msgs {
|
||||
|
||||
@@ -203,6 +203,17 @@ func wirePanelHub(a *app) {
|
||||
})
|
||||
a.plugins.AttachPanels(hub)
|
||||
declareBuiltinPanels(hub, log)
|
||||
// browser 插件(design/28 §8 B4)的面板声明 + 六个动作:它自己就把「先声明、后登记
|
||||
// 动作」这一步走完(wire_browser_panel.go 一处),因此与 search 的纯声明并列。
|
||||
//
|
||||
// 为什么要挡一下 `cfg == nil`:浏览器面板的配置路径与表单默认值**全部**由
|
||||
// `$DATA_DIR` 推出(wire_browser_panel.go 里 `resolveDataDir(a.cfg)`),而部分只读
|
||||
// 夹具(wire_panels_test.go / wire_plugin_actions_test.go 只填了 plugins/bus/log)
|
||||
// 没有配置 ⇒ 直调会解空指针。生产路径上 wireFrontend 跑在 config.Load 之后,
|
||||
// `a.cfg` 恒非 nil,因此这一支只为「不完整夹具」存在(同 wireBrowser 的取舍)。
|
||||
if a.cfg != nil {
|
||||
wireBrowserPanel(hub, a, a.log)
|
||||
}
|
||||
// 登记动作处理器必须在**声明之后**:登记期会核对「登记的处理器有没有对应声明」,
|
||||
// 顺序反过来只会得到一串「声明还没到」的 WARN。
|
||||
// 日志来源另标一处(动作接线 ≠ 面板中枢):`source=cmd/cyrene/wire_plugin_actions`
|
||||
|
||||
@@ -344,6 +344,13 @@ func (a *app) wireRuntime() error {
|
||||
Thinking: thinking,
|
||||
ReasoningEffort: reasoningEffort,
|
||||
ThinkingShowToUser: thinkingShow,
|
||||
TransformContext: a.browserShotTransform(visionResolver),
|
||||
// 截图闭环(design/28 §7 判据 2 / B3):browser 拍照落盘后,由上一行那个钩子在
|
||||
// **每个模型步之前**把该会话待送的图并入本轮视图(取走即消费)。它只改本轮所见 ⇒
|
||||
// 截图不进 checkpoint、也不进后台记忆提取的输入面(design/28 §4 第 3 条)。
|
||||
// 钩子捕获的就是上面那个 visionResolver,所以这一行必须在它之后;装配细节与
|
||||
// 「删掉它没有任何运行期症状」这条理由见 wire_browser_context.go。
|
||||
//
|
||||
// 按调用(而非整批)准入:敏感与否取决于该次调用的参数
|
||||
// (file 的 read 自主、write 代审),见 toolAdmissionHooks。
|
||||
Hooks: a.toolAdmissionHooks(approvalGate),
|
||||
|
||||
Reference in new issue
Block a user