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 包全绿。
112 lines
3.8 KiB
Go
112 lines
3.8 KiB
Go
// Package sdk —— 反代声明的内核侧登记表。
|
||
//
|
||
// 契约定义在公开 SDK(pubsdk.ProxyDef)。本文件只做两件事:
|
||
//
|
||
// 1. 把公开类型重新导出,让内核侧调用方只需 import 本包。
|
||
// 2. 维护**内置插件**的声明表(它们没有 plugin.json,扫目录发现不了)。
|
||
//
|
||
// 外部插件的声明由反代层直接读 plugin.json 得到,不经过这里 —— 那份声明
|
||
// 是静态的、插件没启动也可见,而内置插件的声明只能运行期拿到。两种来源在
|
||
// 反代层合并(见 webui.buildProxyTable)。
|
||
package sdk
|
||
|
||
import (
|
||
"strings"
|
||
"sync"
|
||
|
||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||
)
|
||
|
||
// ProxyDef 是一条反代声明(见 pubsdk.ProxyDef 的完整文档,含「单一入口原则」)。
|
||
// 与 ConfigDef / ChannelDef / ToolDef 同族:SDK 定契约,内核实现行为。
|
||
type ProxyDef = pubsdk.ProxyDef
|
||
|
||
// 鉴权与 Host 规范化的常量/函数转发(调用方不必两处 import)。
|
||
const (
|
||
ProxyAuthHomeAgent = pubsdk.ProxyAuthHomeAgent
|
||
ProxyAuthNone = pubsdk.ProxyAuthNone
|
||
)
|
||
|
||
// ValidateProxyDef 校验一条声明,返回人类可读的错误(合法时为空)。
|
||
func ValidateProxyDef(d ProxyDef) string { return pubsdk.ValidateProxyDef(d) }
|
||
|
||
// EffectiveProxyAuth 返回生效的鉴权模式(空串按默认 homeagent 处理)。
|
||
func EffectiveProxyAuth(auth string) string { return pubsdk.EffectiveProxyAuth(auth) }
|
||
|
||
// ValidProxyAuth 判断鉴权取值是否合法。
|
||
func ValidProxyAuth(auth string) bool { return pubsdk.ValidProxyAuth(auth) }
|
||
|
||
// NormalizeProxyHost 由插件名推导默认的 Host 标签。
|
||
func NormalizeProxyHost(plugin string) string { return pubsdk.NormalizeProxyHost(plugin) }
|
||
|
||
// ValidProxyHostLabel 校验子域名标签是否合法(DNS label 规则)。
|
||
func ValidProxyHostLabel(label string) bool { return pubsdk.ValidProxyHostLabel(label) }
|
||
|
||
// 内置插件(编译进内核、无 plugin.json)的反代声明表。
|
||
//
|
||
// 版本号 builtinProxyVer 每次变更自增。反代层据此判断缓存的路由表是否过期 ——
|
||
// 比每次请求都重新聚合一遍便宜得多。
|
||
var (
|
||
builtinProxyMu sync.RWMutex
|
||
builtinProxyVer int64
|
||
builtinProxyDefs = map[string][]ProxyDef{}
|
||
)
|
||
|
||
// RegisterBuiltinProxy 登记一个内置插件的服务声明(由 RegisterProxy 转发)。
|
||
//
|
||
// ProxyDef.Name 由调用方保证非空(RegisterProxy 会在缺失时兜底为 "service")。
|
||
func RegisterBuiltinProxy(plugin, name string, d ProxyDef) {
|
||
if plugin == "" || strings.TrimSpace(d.Target) == "" {
|
||
return
|
||
}
|
||
if d.Name == "" {
|
||
d.Name = name
|
||
}
|
||
if d.Name == "" {
|
||
d.Name = "service"
|
||
}
|
||
builtinProxyMu.Lock()
|
||
defer builtinProxyMu.Unlock()
|
||
// 同名重复登记(如自动重启后再次 Start)视为刷新,不重复累积。
|
||
list := builtinProxyDefs[plugin]
|
||
for i := range list {
|
||
if list[i].Name == d.Name {
|
||
list[i] = d
|
||
builtinProxyVer++
|
||
return
|
||
}
|
||
}
|
||
builtinProxyDefs[plugin] = append(list, d)
|
||
builtinProxyVer++
|
||
}
|
||
|
||
// ClearBuiltinProxyDefs 清除某插件的声明(插件停止/卸载时调用)。
|
||
func ClearBuiltinProxyDefs(plugin string) {
|
||
builtinProxyMu.Lock()
|
||
defer builtinProxyMu.Unlock()
|
||
if _, ok := builtinProxyDefs[plugin]; ok {
|
||
delete(builtinProxyDefs, plugin)
|
||
builtinProxyVer++
|
||
}
|
||
}
|
||
|
||
// BuiltinProxyDefs 返回内置声明的快照(plugin → defs)。
|
||
func BuiltinProxyDefs() map[string][]ProxyDef {
|
||
builtinProxyMu.RLock()
|
||
defer builtinProxyMu.RUnlock()
|
||
out := make(map[string][]ProxyDef, len(builtinProxyDefs))
|
||
for k, v := range builtinProxyDefs {
|
||
cp := make([]ProxyDef, len(v))
|
||
copy(cp, v)
|
||
out[k] = cp
|
||
}
|
||
return out
|
||
}
|
||
|
||
// BuiltinProxyVersion 是声明表的版本号,供反代层判断缓存是否过期。
|
||
func BuiltinProxyVersion() int64 {
|
||
builtinProxyMu.RLock()
|
||
defer builtinProxyMu.RUnlock()
|
||
return builtinProxyVer
|
||
}
|