mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 04:43:11 +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 包全绿。
74 lines
2.9 KiB
Go
74 lines
2.9 KiB
Go
package plugin
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
|
||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||
)
|
||
|
||
const PackageExt = ".hmap"
|
||
|
||
// SDKProxyDef 是 SDK 反代声明在本包的别名(避免调用方两处 import)。
|
||
type SDKProxyDef = pubsdk.ProxyDef
|
||
|
||
// PluginManifest 每个插件目录中的 plugin.json 元数据。
|
||
type PluginManifest struct {
|
||
Name string `json:"name"`
|
||
NameZh string `json:"name_zh,omitempty"`
|
||
NameEn string `json:"name_en,omitempty"`
|
||
Version string `json:"version"`
|
||
Description string `json:"description,omitempty"`
|
||
Author string `json:"author,omitempty"`
|
||
License string `json:"license,omitempty"`
|
||
Homepage string `json:"homepage,omitempty"`
|
||
Repository string `json:"repository,omitempty"`
|
||
Entry string `json:"entry"` // "plugin.bin"(子进程) | "main.lua" | "SKILL.md"
|
||
Platforms []string `json:"platforms,omitempty"` // 声明的支持平台: ["linux","darwin","windows"]
|
||
MinVersion string `json:"min_version,omitempty"`
|
||
Tags []string `json:"tags,omitempty"`
|
||
Deprecated bool `json:"deprecated,omitempty"`
|
||
|
||
// Proxies 声明本插件需要 HomeAgent 反代出去的服务(自带 Web UI / HTTP API)。
|
||
//
|
||
// 契约定义在公开 SDK(sdk.ProxyDecl),这里只做载体:HomeAgent 加载插件时
|
||
// 读取并聚合,按 Host 子域路由从 webui 的同一端口转发出去。**不声明 = 不被反代**。
|
||
//
|
||
// 字段解析忽略未知键(本仓无 DisallowUnknownFields),因此加这个字段
|
||
// 对「旧内核读新插件」与「新内核读旧插件」都是无害的。
|
||
Proxies []SDKProxyDef `json:"proxies,omitempty"`
|
||
|
||
// Capabilities 声明本插件需要的内核能力组(§3.8 权限梯度)。
|
||
//
|
||
// 取值见 internal/plugin/proc.KnownCapabilities():
|
||
// io / memory / doc_memory / knowledge / text_memory / llm / social /
|
||
// events / plugin_mgr / settings_cross
|
||
//
|
||
// **省略或为空 = 不受限**,而不是「只有基础能力」。
|
||
// 理由:17 个存量插件的 plugin.json 都没有这个字段,若空声明当作最小权限,
|
||
// 它们会全部失去 IO 注入、记忆读写等能力而**静默降级**——
|
||
// 违反「外部插件零改动」的硬约束。收紧的路径是让插件显式声明。
|
||
//
|
||
// core(注册自身工具/阶段/通道 + 读写自己的配置)无需声明,始终可用。
|
||
Capabilities []string `json:"capabilities,omitempty"`
|
||
}
|
||
|
||
func ReadManifest(dir string) (*PluginManifest, error) {
|
||
data, err := os.ReadFile(filepath.Join(dir, "plugin.json"))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var m PluginManifest
|
||
if err := json.Unmarshal(data, &m); err != nil {
|
||
return nil, err
|
||
}
|
||
return &m, nil
|
||
}
|
||
|
||
// IsPluginDir 判断目录是否为有效的插件目录(包含 plugin.json)
|
||
func IsPluginDir(dir string) bool {
|
||
_, err := os.Stat(filepath.Join(dir, "plugin.json"))
|
||
return err == nil
|
||
}
|