mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-26 20:33:15 +00:00
用户要求用方案 A(路径挂载)让 huawei 插件 UI 在外部可用,
并把「通过反代的插件必须使用单一入口」写入 SDK 声明。
## 实测暴露的两个真问题
1. **Path 的语义不能一刀切**。原设计「原样保留」只对**机器接口**成立
(设备客户端硬编码 /api/v1/device/ws,不可能知道反代的存在);
而自带 UI 的服务需要**剥掉前缀**(/p/huawei/api/status → 上游 /api/status)。
猜错的结果是全部请求 404,且看起来像上游故障 —— 所以由声明者选:
strip_path=false 别名模式 / true 前缀模式。非法组合被 validate 挡住。
2. **前缀模式的尾斜杠是必需的**(自测发现的 bug)。
访问 /p/huawei(无尾斜杠)时页面能开,但页面里所有 fetch 都 404 ——
相对路径以「当前文档目录」为基准,没尾斜杠时浏览器把最后一段当文件名,
目录退回上一级,fetch('api/status') 打到 /p/api/status。
修:前缀模式且路径恰等于前缀时 301 到 /p/huawei/(保留查询串)。
**别名模式不做此事** —— 那类路径是上游真实语义,加斜杠会改坏它。
## 插件侧(huawei_smarthome)
- 前端 4 处根绝对路径(fetch('/api/status') 等)改为相对路径,
基准由 location.pathname 推导(BASE)。这是 Path 形态能成立的**前提** ——
否则请求会打到门户自己身上。
- plg.json 声明:host + path=/p/huawei + strip_path=true + auth=homeagent。
- SDK 升到 1.4.0,并用 hmapdev 1.4.0 重新打包(1.3.0 的 hmapdev 无
proxies 支持,会把声明**静默丢弃** —— 实测确认过,这是打包链路上
一个不报警的坑,值得记住)。
## 判据
+6 条:TestProxyPathAliasVsStrip(两态各自正确)、
TestProxyPathLongestPrefixWins(/p/app 不得劫持 /p/apple,
且长前缀胜出)、TestProxyStripPathRedirectsToTrailingSlash(尾斜杠,
含查询串保留 + 别名模式不得重定向)。
变异验证(4 条,均按预期打红后还原回绿):
- 删尾斜杠重定向 → 判红(还原了真实 bug 形态)
- 让别名模式也重定向 → 判红(设备网关语义被毁)
- 从 hmapdev schema 探测体删 StripPath → 判红(漂移检测有效)
- 删 SDK 里的「单一入口原则」字样 → 判红(契约不能只剩口头约定)
全量:35 包全绿。
64 lines
2.3 KiB
Go
64 lines
2.3 KiB
Go
package sdk
|
||
|
||
import "testing"
|
||
|
||
// 内置插件的运行期声明必须真的被登记、可枚举、可清除——
|
||
// remotedevice(内置、无 plugin.json)就靠这条通道。
|
||
func TestBuiltinProxyDefRegistry(t *testing.T) {
|
||
const p = "test_builtin_proxy"
|
||
ClearBuiltinProxyDefs(p)
|
||
defer ClearBuiltinProxyDefs(p)
|
||
|
||
before := BuiltinProxyVersion()
|
||
RegisterBuiltinProxy(p, "gw", ProxyDef{Name: "gw", Host: "devices", Target: "127.0.0.1:9890", WebSocket: true, Auth: ProxyAuthNone})
|
||
if BuiltinProxyVersion() == before {
|
||
t.Error("登记后版本号应递增(反代层靠它判断缓存失效)")
|
||
}
|
||
|
||
got := BuiltinProxyDefs()
|
||
list := got[p]
|
||
if len(list) != 1 {
|
||
t.Fatalf("登记了 %d 条,期望 1: %+v", len(list), got)
|
||
}
|
||
if !list[0].WebSocket || list[0].Auth != ProxyAuthNone || list[0].Host != "devices" {
|
||
t.Errorf("声明内容不对: %+v", list[0])
|
||
}
|
||
|
||
// 重复登记同名(如自动重启后再次 Start)应为刷新而非累积
|
||
RegisterBuiltinProxy(p, "gw", ProxyDef{Name: "gw", Host: "devices", Target: "127.0.0.1:9890", WebSocket: true, Auth: ProxyAuthNone})
|
||
if l := BuiltinProxyDefs()[p]; len(l) != 1 {
|
||
t.Errorf("重复登记应为刷新,实际累积成 %d 条", len(l))
|
||
}
|
||
|
||
// 空 target 必须被拒(不声明的默认就是不被反代,空声明更不该登记)
|
||
RegisterBuiltinProxy(p, "bad", ProxyDef{Name: "bad", Target: " "})
|
||
if l := BuiltinProxyDefs()[p]; len(l) != 1 {
|
||
t.Errorf("空 target 不应被登记,实际 %d 条", len(l))
|
||
}
|
||
|
||
ClearBuiltinProxyDefs(p)
|
||
if _, ok := BuiltinProxyDefs()[p]; ok {
|
||
t.Error("清除后不应还有该插件的声明")
|
||
}
|
||
}
|
||
|
||
// RegisterProxy 必须把声明转发给内核注入的注册回调,且把 name 落进 def
|
||
// (与 RegisterTool 的风格一致:name 同时来自参数与 def.Name)。
|
||
func TestRegisterProxyForwards(t *testing.T) {
|
||
var gotName string
|
||
var got []ProxyDef
|
||
ps := New("demo", SDKConfig{})
|
||
ps.SetProxyRegistrar(func(name string, d ProxyDef) {
|
||
gotName = name
|
||
got = append(got, d)
|
||
})
|
||
|
||
ps.RegisterProxy("ui", ProxyDef{Name: "ui", Host: "demo", Target: "127.0.0.1:12100", Auth: ProxyAuthNone})
|
||
if len(got) != 1 || got[0].Host != "demo" || got[0].Auth != ProxyAuthNone {
|
||
t.Fatalf("声明未转发到注册回调: %+v", got)
|
||
}
|
||
if gotName != "ui" {
|
||
t.Errorf("name 参数未透传: %q", gotName)
|
||
}
|
||
}
|