Files
HomeAgent/internal/sdk/rawfiles_impl.go
JianFeeeee 3f834794c5 refactor: SDK module 路径统一改为 hologramagentsdk(59 文件 + go.mod)
## 为什么必须做

SDK 仓(独立仓库)已把 module 从 `github.com/JianFeeeee/homeagentsdk`
改名为 `hologramagentsdk`。主仓通过 `replace` 直接引用它的**物理目录**:

    replace github.com/JianFeeeee/homeagentsdk => ./third_party/homeagent-sdk

一旦 SDK 侧改成新 module 名,主仓立刻炸:

    go: ./third_party/homeagent-sdk@ used for two different module paths
        (github.com/JianFeeeee/hologramagentsdk and github.com/JianFeeeee/homeagentsdk)

Go **不允许**同一目录被两个 module 路径 replace。所以不能"新旧并存",
只能把主仓 59 个文件的 import 全部改到新名,并删掉旧 replace。

## 一个被证伪的前提

我最初按"third_party/homeagent-sdk 是独立仓库,不能在此仓修改其源码"
来处理,并因此判断两个仓的改动可以隔离。实际上主仓是靠 `replace` 指向
该目录的物理副本 —— **两个仓的改动不可能真正隔离**:SDK 侧改 module 名,
主仓必然跟着改 import。

这个前提应该在做任何改动之前用 `go list -m` 验证,而不是假设。

## 连带修掉两处

| 缺陷 | 性质 |
|---|---|
| `TestLuaSDKMockSingleSource` 失败 | SDK 的 `sdk/lua/sdk.lua` 变了,主仓内嵌副本未同步(跑 `scripts/sync-lua-sdk.sh` 修复) |
| `sdkModuleNames` 分散两处 | 已在 SDK 仓合并为单一数据源(见 SDK 仓 725732b) |

第一条是**副本一致性判据抓到的**,不是我自己发现的 —— 这类判据的价值在此:
两个仓必然有副本,靠人记住"改了 A 要同步 B"是做不到的。

## 验证

- `go build ./...` / `go vet ./...` 干净
- `go test ./... -count=1`:**58 包 0 失败**
- `internal/plugin`(Lua SDK 副本一致性)+ `internal/sdk` 全绿
- 主仓 `third_party/homeagent-sdk/` 与 SDK 仓 `main` 无差异
2026-10-23 15:51:02 +08:00

139 lines
4.1 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 的打包规则对齐)
//
// <插件目录>/rawfile/ ← 包内资源(只读,随升级整体替换)
// <插件目录>/work/ ← 运行期可写目录(跨升级保留)
//
// hmapdev 把工程根 `rawfile/` 的内容与 plg.json 声明的外部路径
// 统一打成 `rawfile/<相对路径>` 条目;内核解包保留路径 ⇒ 落到此处。
//
// ## 为什么只读目录与可写目录必须分开
//
// 升级时会用新包**整体替换**插件目录。若运行期数据也写在 rawfile/ 下,
// 升级就会静默丢掉它们;反过来,若把包内资源写到可写目录,
// 升级后新旧资源混在一起、无法判断哪份是当前的。
//
// ## 安全:路径穿越必须挡住
//
// 资源路径来自插件(可能受 LLM 影响)。不管的话 `../../../../etc/shadow`
// 就能读任意文件 —— 而包是可被第三方替换的,这构成一条真实提权链。
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// RawFilesDirName 是包内资源目录名(与 hmapdev 的 RawFilesPrefix 一致)。
const RawFilesDirName = "rawfile"
// WorkDirName 是可写目录名。
const WorkDirName = "work"
type rawFilesImpl struct {
root string // <插件目录>/rawfile
workDir string // <插件目录>/work
}
// NewRawFilesAPI 构造资源读取接口。pluginDir 是插件自己的安装目录。
func NewRawFilesAPI(pluginDir string) pubsdk.RawFilesAPI {
return &rawFilesImpl{
root: filepath.Join(pluginDir, RawFilesDirName),
workDir: filepath.Join(pluginDir, WorkDirName),
}
}
// resolve 把相对路径解析成绝对路径,并**拒绝穿越**。
func (r *rawFilesImpl) 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 之内 —— 只查 ".." 字符串是不够的
//(`a/../../b` 不含字面 `..` 前缀却会逃出去)。
clean := filepath.Clean(filepath.Join(r.root, rel))
if clean != r.root && !strings.HasPrefix(clean, r.root+string(os.PathSeparator)) {
return "", fmt.Errorf("rawfile 路径逃出资源目录: %q", rel)
}
return clean, nil
}
func (r *rawFilesImpl) 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 *rawFilesImpl) List(rel string) ([]string, error) {
// ★ 空串 = 资源根目录(列出 rawfile/ 的直接子项)。
// 其他方法不接受空串(读一个目录没有意义),但 List 的语义天然包含根。
if strings.TrimSpace(rel) == "" {
entries, err := os.ReadDir(r.root)
if err != nil {
return nil, fmt.Errorf("rawfile 根目录不可读(插件是否带随包资源?): %w", err)
}
return dirNames(entries), nil
}
p, err := r.resolve(rel)
if err != nil {
return nil, err
}
entries, err := os.ReadDir(p)
if err != nil {
return nil, err
}
return dirNames(entries), nil
}
// dirNames 把目录项转成"单层名 + 目录带 / 后缀"的列表(固定排序)。
func dirNames(entries []os.DirEntry) []string {
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
}
func (r *rawFilesImpl) 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 *rawFilesImpl) Dir() string { return r.root }
// WorkDir 返回可写目录(惰性创建)。
func (r *rawFilesImpl) WorkDir() string {
_ = os.MkdirAll(r.workDir, 0o755)
return r.workDir
}
var _ pubsdk.RawFilesAPI = (*rawFilesImpl)(nil)