mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
## 为什么必须做
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` 无差异
139 lines
4.1 KiB
Go
139 lines
4.1 KiB
Go
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)
|