Files
homeagent-sdk/sdk/rawfiles.go
JianFeeeee 0111ac3a7a 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` +§协议通道纪律(正误代码对比)
2026-10-17 16:34:29 +08:00

57 lines
2.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
// RawFilesAPI 让插件读写**随包分发的资源**(rawfile)。
//
// ## 是什么
//
// `hmapdev build` 会把工程根 `rawfile/` 下的全部内容(以及 plg.json
// `rawfiles` 声明的外部路径)打进 .hmap;内核解包时保留路径,落到:
//
// <插件目录>/rawfile/<原相对路径>
//
// 本接口就是插件读这些资源的唯一入口。
//
// ## 为什么不让插件自己拼路径
//
// 插件**不知道自己的安装目录**:内核的 `PluginDir()` 是给插件管理器用的,
// 不暴露给插件自身。而且"包内资源"与"运行期生成的文件"必须分开 ——
// 前者只读、随升级替换,后者可写、跨升级保留。混在一个目录里,
// 升级时会误删运行期数据,或把旧资源留在原地。
//
// 所以:
//
// RawFiles().Read("main/prompt.txt") → 读**包内**资源(只读)
// RawFiles().WorkDir() → 拿**可写**工作目录(运行期数据放这)
//
// ## 安全边界
//
// 所有路径都相对 rawfile/ 根,**必须拒绝路径穿越**(`../`)。
// 资源文件来自包,包可能被替换 —— 让插件能借它读到 `/etc/shadow`
// 就是一个真实的提权面。
type RawFilesAPI interface {
// Read 读一个包内资源(路径相对 rawfile/ 根)。
//
// 例:Read("main/prompt.txt") 读 <插件目录>/rawfile/main/prompt.txt。
Read(rel string) ([]byte, error)
// List 列出某个目录下的条目(相对 rawfile/ 根;"" 表示根)。
//
// 返回的名字是**相对 path 的单层名**(不是全路径),目录名带 `/` 后缀。
List(rel string) ([]string, error)
// Exists 报告某个资源是否存在。
Exists(rel string) bool
// Dir 返回包内资源的根目录(只读)。
//
// 给需要把整个目录交给外部工具的插件用(如把 node 目录的路径
// 传给 exec.Command)。**不要往这里写东西** —— 它在包里,随升级替换。
Dir() string
// WorkDir 返回一个**可写**目录,供插件放运行期生成的文件。
//
// 与 Dir() 分开的原因:Dir() 里的东西随升级被整体替换,
// 运行期数据放那里会在下次升级时丢失。
WorkDir() string
}