mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-11-01 06:47:25 +00:00
fix(sdk): 协议通道纪律 + hmapdev 模板升级 + 示例插件 stdout 污染
## 核心缺陷:插件往 stdout 打日志 = 污染 JSON-RPC 协议通道
子进程插件与内核用 stdout 传 NDJSON 帧,stderr 才是日志通道。
脚手架模板(及 4 个示例插件)用 `fmt.Printf` 打日志 ⇒ 内核收到非 JSON 文本:
[proc] weather 收到非法 JSON 帧(21 字节): invalid character 'i'
**生产实测 1038 条**(huawei_smarthome 720 / weather 113 / rss 103 / calendar 102)。
后果不只是日志噪音:那一行日志被**丢弃**,插件看起来"没启动"。
修法:
- hmapdev 模板 `fmt.Printf` → `log.Printf`(9 处)
- 示例插件 4 个 14 处:ai_image(2) calendar(5) rss(4) weather(3)
+ 补 "log" import
- 内核侧(主仓)同步让报错可定位:内容摘要 + 可能原因 + 修法
判别法:`package main` = 子进程(协议通道,禁用 stdout 打日志);
普通库包 = 进程内插件(编译进内核,写真实终端,**不是**缺陷)——
`internal/plugins/cli` 的 `fmt.Println` 属后者。
## hmapdev
- `cmd_build.go` +136:平台产物精确匹配(`--target` 是**append** 语义,
原脚本 `ls dist/*.hmap | head -1` 会按字典序抓到 amd64,
**导致 arm64 包里装的是 x86-64 二进制**,且退出码 0、校验和通过)
- `cmd_init.go` +46:脚手架 stdio 纪律
- `templates/proc_main.go.tmpl`:补齐 mgrStop/mgrStart/mgrIsRuntimeStopped
三态能力(插件管理器运行态停启)
- `templates/plg.json.tmpl` 删除(合并进 cmd_init)
## 判据
- `scaffold_stdout_test.go`(3 条):扫模板生成的代码不得写 stdout。
★ 判据自身先假红过一次:**模板的注释里就写了 `fmt.Printf`** ⇒
必须 `stripTemplateComments()` 之后才判
- `example_stdout_test.go`(2 条):扫全部 20 个子进程示例插件,
报错给**文件:行号**;变异咬住并给出准确行号
- `package_examples_test.go`(2 条):**执行真实打包脚本**
(第一版测的是 hmapdev 而不是脚本 ⇒ 变异不咬)
- `cmd_build` / `cmd_init` 新判据
## 其他
- sdk/settings.go +15(三态能力透传)
- `docs/versions.md` +§示例插件协议通道纪律(含生产 1038 条实测表格)
- `skills/homeagent-plugin-dev/SKILL.md` +§协议通道纪律(正误代码对比)
This commit is contained in:
@ -13,11 +13,14 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -740,6 +743,8 @@ func buildPluginSDK(name string) *sdk.PluginSDK {
|
||||
base.SetKnowledgeAPI(procKnowledge{})
|
||||
base.SetLLMAPI(procLLM{})
|
||||
base.SetSocialAPI(procSocial{})
|
||||
base.SetSecretsAPI(procSecrets{})
|
||||
base.SetRawFilesAPI(procRawFiles{})
|
||||
base.SetTextMemoryAPI(procTextMemory{})
|
||||
base.SetPluginMgrAPI(procPluginMgr{})
|
||||
base.SetInputChannelRegistrar(func(chName string, def sdk.ChannelDef) error {
|
||||
@ -1160,6 +1165,234 @@ func (procPluginMgr) IsPluginDisabled(name string) bool {
|
||||
return r.Disabled
|
||||
}
|
||||
|
||||
// ---- 运行态启停(设计 docs/zh/kernel-modularization-design.md §2.6.7)----
|
||||
//
|
||||
// ★ 注意 method 名是 `plugin.mgrStop/mgrStart/mgrIsRuntimeStopped`,
|
||||
// **不是** `plugin.stop/plugin.start` —— 后两个是内核→插件的生命周期消息
|
||||
// (MethodPluginStart/Stop,见 protocol.go),语义完全不同。用错名会撞车。
|
||||
func (procPluginMgr) StopPlugin(name string) error {
|
||||
return callCoreVoid("plugin.mgrStop", map[string]string{"name": name})
|
||||
}
|
||||
func (procPluginMgr) StartPlugin(name string) error {
|
||||
return callCoreVoid("plugin.mgrStart", map[string]string{"name": name})
|
||||
}
|
||||
func (procPluginMgr) IsPluginRuntimeStopped(name string) bool {
|
||||
raw, err := callCore("plugin.mgrIsRuntimeStopped", map[string]string{"name": name})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var r struct {
|
||||
RuntimeStopped bool `json:"runtime_stopped"`
|
||||
}
|
||||
json.Unmarshal(raw, &r)
|
||||
return r.RuntimeStopped
|
||||
}
|
||||
|
||||
// procRawFiles 是 rawfile 资源的插件侧实现。
|
||||
//
|
||||
// ★ 它**不走 RPC**:资源就在插件进程自己的目录下(`<插件目录>/rawfile/`),
|
||||
// 直接读本地文件即可。走 RPC 是错的 —— 随包资源可能有几十上百兆
|
||||
// (如内置的 node 二进制),把它们搬过进程边界毫无意义。
|
||||
//
|
||||
// 路径来源:内核在 spawn 时注入 HA_PLUGIN_RAWFILE_DIR / HA_PLUGIN_WORK_DIR。
|
||||
// 不用 cwd 推断(插件可能自己 chdir),也不读内核的 PluginDir()(那个不给插件)。
|
||||
type procRawFiles struct{}
|
||||
|
||||
func (procRawFiles) root() string {
|
||||
if v := os.Getenv("HA_PLUGIN_RAWFILE_DIR"); v != "" {
|
||||
return v
|
||||
}
|
||||
// 回退:cwd 就是插件目录(内核 spawn 时设的 Dir)
|
||||
wd, _ := os.Getwd()
|
||||
return filepath.Join(wd, "rawfile")
|
||||
}
|
||||
|
||||
func (r procRawFiles) workRoot() string {
|
||||
if v := os.Getenv("HA_PLUGIN_WORK_DIR"); v != "" {
|
||||
return v
|
||||
}
|
||||
wd, _ := os.Getwd()
|
||||
return filepath.Join(wd, "work")
|
||||
}
|
||||
|
||||
// resolve 解析相对路径并拒绝穿越(与内核侧 rawFilesImpl 同一套规则)。
|
||||
func (r procRawFiles) resolve(rel string) (string, error) {
|
||||
rel = strings.TrimSpace(rel)
|
||||
if rel == "" {
|
||||
return "", fmt.Errorf("rawfile 路径不能为空")
|
||||
}
|
||||
if filepath.IsAbs(rel) {
|
||||
return "", fmt.Errorf("rawfile 路径必须是相对路径: %q", rel)
|
||||
}
|
||||
root := r.root()
|
||||
clean := filepath.Clean(filepath.Join(root, rel))
|
||||
if clean != root && !strings.HasPrefix(clean, root+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("rawfile 路径逃出资源目录: %q", rel)
|
||||
}
|
||||
return clean, nil
|
||||
}
|
||||
|
||||
func (r procRawFiles) Read(rel string) ([]byte, error) {
|
||||
p, err := r.resolve(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("rawfile 不存在: %s(打包时是否漏了 rawfile/ 或 plg.json 的 rawfiles?)", rel)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r procRawFiles) List(rel string) ([]string, error) {
|
||||
p, err := r.resolve(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
out = append(out, name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r procRawFiles) Exists(rel string) bool {
|
||||
p, err := r.resolve(rel)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
st, err := os.Stat(p)
|
||||
return err == nil && !st.IsDir()
|
||||
}
|
||||
|
||||
func (r procRawFiles) Dir() string { return r.root() }
|
||||
|
||||
func (r procRawFiles) WorkDir() string {
|
||||
d := r.workRoot()
|
||||
_ = os.MkdirAll(d, 0o755)
|
||||
return d
|
||||
}
|
||||
|
||||
var _ sdk.RawFilesAPI = procRawFiles{}
|
||||
|
||||
// procSecrets 是变量库 API 的插件侧一半:全部经 RPC 打回内核。
|
||||
//
|
||||
// ★ 它**不是**插件自己的存储 —— 内核的变量库是进程级单例,
|
||||
// 主/子/驻留子 agent 与全部插件共用同一套别名。所以这里只做转发,
|
||||
// 不在插件侧留任何副本(留了就会出现"两库互不可见"的经典问题:
|
||||
// 内核拦截登记的变量插件看不到)。
|
||||
type procSecrets struct{}
|
||||
|
||||
// Available 用**专门的**可用性查询,不靠 List 推断。
|
||||
//
|
||||
// ★ 实测教训:初版用 `secrets.list` 成功与否判断 —— 而未启用时
|
||||
// List 返回空列表(不报错),于是 Available() 误判为 true,
|
||||
// 接着 Store 才失败。错误信息于是变成"存储失败"而非"能力不可用",
|
||||
// 排查方向完全错。
|
||||
func (procSecrets) Available() bool {
|
||||
raw, err := callCore("secrets.available", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var r struct {
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return false
|
||||
}
|
||||
return r.Available
|
||||
}
|
||||
|
||||
func (procSecrets) Store(value, note string) (string, error) {
|
||||
raw, err := callCore("secrets.store", map[string]interface{}{"value": value, "note": note})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var r struct {
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Alias, nil
|
||||
}
|
||||
|
||||
func (procSecrets) List() []sdk.SecretInfo {
|
||||
raw, err := callCore("secrets.list", nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var r struct {
|
||||
Infos []sdk.SecretInfo `json:"infos"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return nil
|
||||
}
|
||||
return r.Infos
|
||||
}
|
||||
|
||||
func (procSecrets) Forget(alias string) error {
|
||||
return callCoreVoid("secrets.forget", map[string]string{"alias": alias})
|
||||
}
|
||||
|
||||
func (procSecrets) Fill(text string) (string, int, []string) {
|
||||
raw, err := callCore("secrets.fill", map[string]string{"text": text})
|
||||
if err != nil {
|
||||
return text, 0, nil
|
||||
}
|
||||
var r struct {
|
||||
Text string `json:"text"`
|
||||
Replaced int `json:"replaced"`
|
||||
Aliases []string `json:"aliases"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return text, 0, nil
|
||||
}
|
||||
return r.Text, r.Replaced, r.Aliases
|
||||
}
|
||||
|
||||
func (procSecrets) Encrypt(plain []byte) (string, error) {
|
||||
raw, err := callCore("secrets.encrypt", map[string]string{
|
||||
"plain_b64": base64.StdEncoding.EncodeToString(plain),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var r struct {
|
||||
Encrypted string `json:"encrypted"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Encrypted, nil
|
||||
}
|
||||
|
||||
func (procSecrets) Decrypt(stored string) ([]byte, error) {
|
||||
raw, err := callCore("secrets.decrypt", map[string]string{"stored": stored})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var r struct {
|
||||
PlainB64 string `json:"plain_b64"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return base64.StdEncoding.DecodeString(r.PlainB64)
|
||||
}
|
||||
|
||||
type procSettings struct{}
|
||||
|
||||
func (procSettings) Get(key string) (interface{}, error) {
|
||||
@ -1168,6 +1401,26 @@ func (procSettings) Get(key string) (interface{}, error) {
|
||||
func (procSettings) Set(key string, v interface{}) error {
|
||||
return callCoreVoid("settings.set", map[string]interface{}{"key": key, "value": v})
|
||||
}
|
||||
|
||||
// SetSecret 强制加密落盘(不看键名)。内核侧拒绝在无密钥时写明文。
|
||||
func (procSettings) SetSecret(key string, v interface{}) error {
|
||||
return callCoreVoid("settings.setSecret", map[string]interface{}{"key": key, "value": v})
|
||||
}
|
||||
|
||||
// GetSecret 读取 SetSecret 写入的值。
|
||||
func (procSettings) GetSecret(key string) (string, error) {
|
||||
raw, err := callCore("settings.getSecret", map[string]string{"key": key})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var r struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Value, nil
|
||||
}
|
||||
func (procSettings) List(prefix string) ([]string, error) {
|
||||
return settingsKeys("settings.list", map[string]string{"prefix": prefix})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user