Merge pull request #26 from JianFeeeee/fix/webp-embed-and-migrate-log

fix(media): webp 解码支持 + 迁移失败可观测 + 补回遗漏的 module 路径统一
This commit is contained in:
JianFeeeee
2026-10-23 15:56:07 +08:00
committed by GitHub
71 changed files with 366 additions and 111 deletions

14
go.mod
View File

@ -13,12 +13,12 @@ require github.com/yanyiwu/gojieba v1.4.7
require github.com/yalue/onnxruntime_go v1.13.0
require (
github.com/JianFeeeee/homeagentsdk v0.9.2
github.com/JianFeeeee/hologramagentsdk v0.9.2
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/sys v0.38.0
golang.org/x/text v0.3.8
golang.org/x/text v0.23.0
)
require (
@ -41,6 +41,14 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/image v0.25.0 // indirect
)
replace github.com/JianFeeeee/homeagentsdk => ./third_party/homeagent-sdk
// SDK 仓 module 已改名为 hologramagentsdk(与 GitHub 仓名一致)。
//
// replace 指向本地源码副本,使主仓改动与 SDK 改动可在同一工作树内联调。
// ★ 只能有这一条:Go 不允许同一目录被两个 module 路径 replace
// (报 `used for two different module paths`)。所以主仓**所有** import 都已
// 统一改为新名,旧的 homeagentsdk 路径不再保留。
replace github.com/JianFeeeee/hologramagentsdk => ./third_party/homeagent-sdk

4
go.sum
View File

@ -56,12 +56,16 @@ github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -10,7 +10,7 @@ import (
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
"github.com/JianFeeeee/HologramAgent/internal/events"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// eventLoop 已由 scheduler.go 的 schedulerLoop 取代(M2)。

View File

@ -9,7 +9,7 @@ import (
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/memory/document"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 统一输入主干(processInput / resolveInput / injectedBlocks)与

View File

@ -1,12 +1,14 @@
package core
import (
"errors"
"fmt"
"log"
"sync"
"sync/atomic"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/memory/vector"
safeclose "github.com/JianFeeeee/HologramAgent/internal/safeclose"
)
@ -67,19 +69,41 @@ func (a *Agent) reembedStaleMedia() {
var jobsOnce sync.Once
jobs := make(chan memory.MemoryBlock, workers*2)
var done, failed int64
var done, failed, unsupported int64
var wg sync.WaitGroup
// 失败原因按错误文本聚合 —— 不逐条打日志(上千张图会刷屏),
// 但**必须留下"为什么失败"**。
//
// ★ 2026-10-23 实测教训:原先这里只 `failed++` 然后 continue,于是
// 「向量迁移完成: 成功=0 失败=4」成了唯一的线索 —— 没有任何日志说明
// 这 4 个是 webp(当时 chineseclip 未注册 webp 解码器)。失败不可观测
// 直接导致这次排查要从数据库里反查 mime 才找到根因。
var failMu sync.Mutex
failReasons := map[string]int{}
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for b := range jobs {
if err := a.reembedOneBlock(b, fp); err != nil {
atomic.AddInt64(&failed, 1)
err := a.reembedOneBlock(b, fp)
if err == nil {
atomic.AddInt64(&done, 1)
continue
}
atomic.AddInt64(&done, 1)
// 模态不受支持是**永久**状态(不是本次网络/服务抖动):
// 这类块永远不会有向量,语义检索也永远搜不到它。
// 单独计数,避免它混在"失败"里让人以为重试就能好。
if errors.Is(err, vector.ErrModalityUnsupported) {
atomic.AddInt64(&unsupported, 1)
} else {
atomic.AddInt64(&failed, 1)
}
key := b.MIME + " | " + err.Error()
failMu.Lock()
failReasons[key]++
failMu.Unlock()
}
}()
}
@ -93,8 +117,17 @@ func (a *Agent) reembedStaleMedia() {
}
safeclose.Safe(&jobsOnce, jobs)
wg.Wait()
log.Printf("[media] 向量迁移完成: 成功=%d 失败=%d 总计=%d fp=%s",
done, failed, len(targets), shortFP(fp))
log.Printf("[media] 向量迁移完成: 成功=%d 失败=%d 不支持=%d 总计=%d fp=%s",
done, failed, unsupported, len(targets), shortFP(fp))
// 逐类打印失败原因(聚合后通常只有 1~2 类)。
// 没有这段,运维只能看到"失败=N"而不知为何 —— 本次 webp 问题的排查成本
// 全花在这里。
if len(failReasons) > 0 {
log.Printf("[media] 失败原因分类(按 mime | error 聚合):")
for reason, n := range failReasons {
log.Printf("[media] %d 个: %s", n, reason)
}
}
}
// reembedOneBlock 为一个图像块重算向量并写回。负载就在块里,无需外部取字节。

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"path/filepath"
"regexp"
"strings"
"testing"
@ -102,3 +103,100 @@ func TestReembedStaleMedia_NilSpaceNoop(t *testing.T) {
t.Errorf("无空间可用时不该写向量: vec=%v model=%q", got.Vector, got.Fingerprint)
}
}
// failSpace 让指定 mime 的图像嵌入失败,用来验证「失败必须留下原因」。
type failSpace struct {
failMIME string
}
func (failSpace) VectorizeDense(string) ([]float64, error) { return []float64{1, 0}, nil }
func (f failSpace) EmbedImageDense(_ []byte, mime string) ([]float64, error) {
if mime == f.failMIME {
return nil, fmt.Errorf("模拟解码失败: %s", mime)
}
return []float64{1, 0}, nil
}
func (failSpace) Fingerprint() string { return "fail-space" }
func (failSpace) Dim() int { return 2 }
func (failSpace) Loaded() bool { return true }
func (failSpace) Close() {}
// 迁移失败必须留下**可诊断的原因**,而不只是一个计数。
//
// ★ 2026-10-23 生产事故:日志只有
//
// [media] 向量迁移完成: 成功=0 失败=4 总计=4
//
// 没有任何信息说明为什么。反查数据库才发现那 4 个块是 image/webp,
// 而 provider 没注册 webp 解码器。**失败不可观测直接导致排查成本翻倍**。
//
// 本判据钉住:失败原因按 "mime | error" 聚合后必须出现在日志里。
//
// 变异:把 failReasons 的采集与打印删掉 ⇒ 红。
func TestReembedStaleMedia_LogsFailureReasons(t *testing.T) {
a := newMediaLoopAgent(t)
// 两个都会失败(同一 mime)—— 应聚合成一条 "2 个: ..."
for i := 0; i < 2; i++ {
b := mediaBlock("image/webp", []byte("webp-bytes"))
if err := a.memory.PutMemoryBlocks([]memory.MemoryBlock{b}); err != nil {
t.Fatal(err)
}
}
logs := captureLog(t, func() {
a.multimodalSpace = failSpace{failMIME: "image/webp"}
a.reembedStaleMedia()
})
if !strings.Contains(logs, "失败原因分类") {
t.Fatalf("迁移失败时必须打印失败原因分类,实际日志:\n%s", logs)
}
if !strings.Contains(logs, "image/webp") {
t.Fatalf("失败原因里必须出现具体 mime(否则无从定位是哪种格式),实际日志:\n%s", logs)
}
if !strings.Contains(logs, "模拟解码失败") {
t.Fatalf("失败原因里必须带上 provider 返回的错误原文,实际日志:\n%s", logs)
}
// 计数必须出现(格式 "N 个: ...")—— 具体数字取决于去重后的块数,
// 这里只要求"有计数",不绑死数字(绑死会让判据随去重策略变化而脆)。
if !regexp.MustCompile(`\d+ 个: image/webp`).MatchString(logs) {
t.Fatalf("失败原因应带聚合计数(形如「N 个: image/webp | ...」),实际日志:\n%s", logs)
}
}
// 不支持模态与临时失败必须**分开计数**:前者永远好不了(重试无意义),
// 混在"失败"里会让人误以为重试即可恢复。
func TestReembedStaleMedia_SeparatesUnsupportedFromFailed(t *testing.T) {
a := newMediaLoopAgent(t)
// 音频走 fakeSpace 的 ErrModalityUnsupported 分支;但 reembedStaleMedia
// 只挑 image 模态,所以用图像 mime + 让 provider 返回 unsupported 来验证分类。
img := mediaBlock("image/tiff", []byte("tiff-bytes"))
if err := a.memory.PutMemoryBlocks([]memory.MemoryBlock{img}); err != nil {
t.Fatal(err)
}
logs := captureLog(t, func() {
a.multimodalSpace = unsupportedImageSpace{}
a.reembedStaleMedia()
})
if !strings.Contains(logs, "不支持=1") {
t.Fatalf("不受支持的模态应单独计数(不支持=1),实际日志:\n%s", logs)
}
}
// unsupportedImageSpace 对任何图像都返回 ErrModalityUnsupported。
type unsupportedImageSpace struct{}
func (unsupportedImageSpace) VectorizeDense(string) ([]float64, error) { return []float64{1, 0}, nil }
func (unsupportedImageSpace) EmbedImageDense(_ []byte, mime string) ([]float64, error) {
return nil, fmt.Errorf("%w: %s", vector.ErrModalityUnsupported, mime)
}
func (unsupportedImageSpace) Fingerprint() string { return "unsupported-space" }
func (unsupportedImageSpace) Dim() int { return 2 }
func (unsupportedImageSpace) Loaded() bool { return true }
func (unsupportedImageSpace) Close() {}

View File

@ -7,7 +7,7 @@ import (
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
"github.com/JianFeeeee/HologramAgent/internal/memory"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// sceneKeysFor 推导本轮输入的**当前场景**。

View File

@ -5,7 +5,7 @@ import (
"testing"
"github.com/JianFeeeee/HologramAgent/internal/meta"
sdkmeta "github.com/JianFeeeee/homeagentsdk/meta"
sdkmeta "github.com/JianFeeeee/hologramagentsdk/meta"
)
func TestExpandPromptVars(t *testing.T) {

View File

@ -4,7 +4,7 @@ import (
"testing"
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 这一组测试锁死「默认不裁剪」这条语义。

View File

@ -8,7 +8,7 @@ import (
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
"github.com/JianFeeeee/HologramAgent/internal/memory"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 这一组测试锁死「默认召回、可显式关闭」这条语义。

View File

@ -15,7 +15,7 @@ import (
"testing"
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
func scenePolicyNoneEvent() *agentIO.InputEvent {

View File

@ -33,7 +33,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/memory"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// Step 是任务状态机的游标。

View File

@ -42,7 +42,7 @@ import (
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// visionBatchProvider 在 batchProvider 之上声明「能看图」。

View File

@ -10,7 +10,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/meta"
"github.com/JianFeeeee/HologramAgent/internal/sdk"
"github.com/JianFeeeee/HologramAgent/internal/security"
sdkmeta "github.com/JianFeeeee/homeagentsdk/meta"
sdkmeta "github.com/JianFeeeee/hologramagentsdk/meta"
)
func (a *Agent) buildMemoryContext(input string, maxTokens int, scenes []string) string {

View File

@ -5,7 +5,7 @@ import (
"testing"
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
sdk "github.com/JianFeeeee/homeagentsdk/sdk"
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// appendPlaceholder 复刻 process() 循环顶部的补位逻辑。

View File

@ -8,7 +8,7 @@ import (
"sync"
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// ErrToolNotFound 表示「工具不存在」(未注册 / 所属插件已卸载或崩溃)。

View File

@ -3,7 +3,7 @@ package io
import (
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// drainOne 取出一条注入事件;没有则 Fatal。

View File

@ -1,5 +1,5 @@
-- HomeAgent Lua Plugin SDK
-- Interface contract between Lua plugins and HomeAgent kernel.
-- HologramAgent Lua Plugin SDK
-- Interface contract between Lua plugins and HologramAgent kernel.
-- !impl functions are replaced by Go implementations at runtime.
-- Standalone/debug: pure Lua mock implementations are used.
-- Usage: local sdk = require("sdk")

View File

@ -6,7 +6,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/plugin/proc"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// EventRing 是 Bus 与 proc.EvtRing 之间的适配层。

View File

@ -5,7 +5,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/plugin/proc"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// TestEventRing_CloseUnsubscribesFromBus 钉死:EventRing.Close 必须把

View File

@ -8,7 +8,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/plugin/proc"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 事件环基础测试:Host 创建事件环 → EventRing 写入 → 消费者读到。

View File

@ -7,7 +7,7 @@ import (
"time"
"github.com/JianFeeeee/HologramAgent/internal/plugin/proc"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
const PackageExt = ".hmap"

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 子进程架构的性能基准(Part 6.6 验收项)。

View File

@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// coreHandler 把插件发来的 RPC 调用路由到内核 PluginSDK。

View File

@ -5,7 +5,7 @@ import (
"fmt"
"github.com/JianFeeeee/HologramAgent/internal/knowledge"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// handleGraphMemory 处理图记忆(实体/关系):recall / commit / introspect / merge / purge。

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// handleRegister 处理注册面:工具 / stage / 输出通道 / 插件 API / 入站通道。

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// handlePluginMgr 处理插件管理:reloadOne / listLoaded / isDisabled。

View File

@ -22,7 +22,7 @@ import (
"encoding/json"
"fmt"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
func (h *coreHandler) secrets(method string, params json.RawMessage) (interface{}, error) {

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// handleSettings 处理全部设置项读写(14 个 method 共用一条实现)。

View File

@ -24,7 +24,7 @@ import (
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
internalSDK "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// secretE2EPlugin 是一个**用真实模板**编译的插件:它只做两件事 ——
@ -32,7 +32,7 @@ import (
const secretE2EPlugin = `package main
import (
sdk "github.com/JianFeeeee/homeagentsdk/sdk"
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
type secretPlugin struct{ name string }

View File

@ -29,14 +29,14 @@ import (
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
internalSDK "github.com/JianFeeeee/HologramAgent/internal/sdk"
internalSecurity "github.com/JianFeeeee/HologramAgent/internal/security"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// secretsE2EPlugin 用真实模板编译:只做变量库往返。
const secretsE2EPlugin = `package main
import (
sdk "github.com/JianFeeeee/homeagentsdk/sdk"
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
type secPlugin struct{ name string }

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 端到端:用**真实 hmapdev 模板**编译的插件,经内核 proc 通道加载运行。
@ -25,7 +25,7 @@ const e2ePluginSource = `package main
import (
"strings"
sdk "github.com/JianFeeeee/homeagentsdk/sdk"
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
type e2ePlugin struct{ name string }
@ -148,8 +148,8 @@ func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
}
mustWriteFile(t, filepath.Join(dir, "go.mod"),
"module e2eplugin\n\ngo 1.25\n\n"+
"require github.com/JianFeeeee/homeagentsdk v0.9.2\n\n"+
"replace github.com/JianFeeeee/homeagentsdk => "+sdkPath+"\n")
"require github.com/JianFeeeee/hologramagentsdk v0.9.2\n\n"+
"replace github.com/JianFeeeee/hologramagentsdk => "+sdkPath+"\n")
bin := filepath.Join(dir, "plugin.bin")
cmd := exec.Command("go", "build", "-o", bin, ".")
@ -241,7 +241,7 @@ func TestE2E_RealTemplatePluginFullLifecycle(t *testing.T) {
func TestE2E_RealTemplateReadOnlyPluginDoesNotOverwrite(t *testing.T) {
const readerSource = `package main
import sdk "github.com/JianFeeeee/homeagentsdk/sdk"
import sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
type readerPlugin struct{ name string }

View File

@ -9,7 +9,7 @@ import (
"time"
safeclose "github.com/JianFeeeee/HologramAgent/internal/safeclose"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// ---- 事件类型编码(编译时确定,与 pubsdk.EventType 一一对应)----

View File

@ -21,7 +21,7 @@ import (
"encoding/json"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
func newTestSeg(t *testing.T) *Segment {

View File

@ -45,7 +45,7 @@ import (
"os/exec"
"time"
sdk "github.com/JianFeeeee/homeagentsdk/sdk"
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
type gcPlugin struct{ name string }
@ -378,7 +378,7 @@ import (
"syscall"
"time"
sdk "github.com/JianFeeeee/homeagentsdk/sdk"
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
type gcPlugin struct{ name string }

View File

@ -8,7 +8,7 @@ import (
"sync"
"sync/atomic"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// Host 持有**被全部子进程插件共享的统一内存区域**,是共享数据面的

View File

@ -5,7 +5,7 @@ import (
"testing"
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// TestHost_CloseWaitsForInflightStage 钉死不变量:

View File

@ -4,7 +4,7 @@ import (
"sync/atomic"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// stubSubscriber 是最小 EvtRingSubscriber 实现,用于验证「关停时统一退订」。

View File

@ -5,7 +5,7 @@ import (
"sync/atomic"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 本文件是 2026-09-04 06:56:18 线上 crash 的回归测试。

View File

@ -10,7 +10,7 @@ import (
"sync/atomic"
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// Plugin 是 registry 可加载的子进程插件,与内置插件同构的启停接口。

View File

@ -8,7 +8,7 @@ import (
"sync"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 端到端验证:内核 RunStage 并发扇出 → 真实子进程插件经共享内存读改写 → 结果回读。

View File

@ -9,7 +9,7 @@ package proc
import (
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
func TestClampExternalPriority_RejectsL4(t *testing.T) {

View File

@ -22,7 +22,7 @@ import (
"strings"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// profileCtx 构造一组「接近真实」的 StageContext。

View File

@ -6,7 +6,7 @@ import (
"sync"
"testing"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 本文件验证共享内存 stage 并发的正确性——**整个迁移最关键的一环**(§4.4 风险 3.4)。

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// StageContext 的跨进程编解码(§3.3 数据面 / §3.4 SDK 封装全部复杂度)。

View File

@ -7,7 +7,7 @@ import (
"sync"
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// stage 执行:把内核的 RunStage 并发扇出接到共享段(§3.4,风险 3.4 的落点)。

View File

@ -6,7 +6,7 @@ import (
"testing"
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// qq 插件「重复申请 stage 锁」的生产诊断与修复(2026-10-07)

View File

@ -6,7 +6,7 @@ import (
"testing"
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 流式输出压测(§4.3 标记「风险高」的那一项)。

View File

@ -3,7 +3,7 @@ package plugin
import (
"github.com/JianFeeeee/HologramAgent/internal/plugin/proc"
isdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// procCore 把内核的 *internal/sdk.PluginSDK 收窄成子进程插件可见的能力面。

View File

@ -11,7 +11,7 @@ import (
"time"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
sdkpub "github.com/JianFeeeee/homeagentsdk/sdk"
sdkpub "github.com/JianFeeeee/hologramagentsdk/sdk"
)
type toolCapture struct {

View File

@ -7,7 +7,7 @@ import (
"testing"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// mockSettings implements pubsdk.SettingsAPI for tests

View File

@ -14,7 +14,7 @@ import (
"testing"
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 真实外部插件(重编为 plugin.bin)经内核加载后的端到端冒烟(Part 6.3)。

View File

@ -12,7 +12,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/events"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// ---- 测试辅助 ----

View File

@ -10,7 +10,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/memory"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// fakeGraphMemory 是一个最小 MemoryAPI 桩,只为把 GraphData 喂给 handler。

View File

@ -15,7 +15,7 @@ import (
"github.com/JianFeeeee/HologramAgent/internal/knowledge"
"github.com/JianFeeeee/HologramAgent/internal/memory"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 记忆面:图记忆 / 文档记忆 / 文本记忆 / 知识库 / LLM 源 / 变更追踪。

View File

@ -1,6 +1,6 @@
package sdk
import pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
import pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
// KernelControlAPI 是内核重启/关机通道(转发公开 SDK 的同名接口)。
//

View File

@ -2,7 +2,7 @@ package sdk
import (
"github.com/JianFeeeee/HologramAgent/internal/knowledge"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// KnowledgeAPI 是内置插件使用的全量知识库接口。

View File

@ -3,7 +3,7 @@ package sdk
import (
"context"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// LLMAPI 是内置插件使用的全量 LLM Provider 接口。

View File

@ -3,7 +3,7 @@ package sdk
import (
"time"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// MemoryBlockView 是块的最小只读视图(供星图轮询端点)。

View File

@ -6,7 +6,7 @@ import (
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
"github.com/JianFeeeee/HologramAgent/internal/events"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// SDKVersion 是对外 SDK 版本号,与核心 meta.Version 保持一致。

View File

@ -14,7 +14,7 @@ import (
"strings"
"sync"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// ProxyDef 是一条反代声明(见 pubsdk.ProxyDef 的完整文档,含「单一入口原则」)。

View File

@ -28,7 +28,7 @@ import (
"sort"
"strings"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// RawFilesDirName 是包内资源目录名(与 hmapdev 的 RawFilesPrefix 一致)。

View File

@ -22,7 +22,7 @@ import (
"fmt"
internalSecurity "github.com/JianFeeeee/HologramAgent/internal/security"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// secretsImpl 实现 pubsdk.SecretsAPI。

View File

@ -1,6 +1,6 @@
package sdk
import pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
import pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
// SettingsAPI 是内置插件使用的全量配置接口。
type SettingsAPI interface {

View File

@ -6,6 +6,17 @@ import (
"image"
// 契约要求 provider 自行解码 Data,所以这里注册常见图像格式。
//
// ★ webp 必须注册(2026-10-23 实测):线上 551 个图像块里有 4 个是
// `image/webp`(由 see_image 抓取的第三方图片),它们**永远嵌不进去** ——
// 启动时向量迁移报「失败=4」,但错误被 continue 吞掉、没有任何日志,
// 只看到数字对不上。根因就是这里少了 webp 注册:
//
// image.Decode(...) → "image: unknown format"
//
// Go 标准库不含 webp 解码器(那是 x/image 的子包),所以这是新增依赖的
// 唯一理由。x/image 是纯 Go、无 cgo,不改变二进制形态。
_ "golang.org/x/image/webp"
_ "image/gif"
_ "image/jpeg"
_ "image/png"

View File

@ -0,0 +1,97 @@
package chineseclip
// webp 解码支持与「失败必须可观测」的判据。
//
// ## 为什么这两条要放在这里
//
// 2026-10-23 线上排查:启动日志只有一行
//
// [media] 向量迁移完成: 成功=0 失败=4 总计=4
//
// 没有任何信息说明为什么失败。反查数据库才发现那 4 个块的 mime 是
// `image/webp`,而本 provider 只注册了 gif/jpeg/png 三个解码器 ⇒
// `image.Decode` 报 "unknown format" ⇒ 它们**永远**拿不到向量,
// 语义检索也永远搜不到它们。
//
// 两个独立缺陷:
// 1. 功能缺口:webp 不可解码
// 2. 可观测性缺口:失败被 `continue` 吞掉,只有计数没有原因
//
// 判据分开钉住,避免以后只修其中一个。
import (
"bytes"
"image"
"image/color"
"image/png"
"testing"
)
// webpBytes 是一个最小的合法 webp 文件(1×1 白色,RIFF/WEBP/VP8L)。
//
// 手写而不依赖测试夹具:夹具文件容易被误删,而"能解码 webp"这件事
// 必须一直成立 —— 线上确实存在 webp 记忆块。
var webpBytes = []byte{
// 由 ffmpeg 生成的真实 2×2 白色 webp(44 字节,VP8 有损编码)。
// 手写字节序列容易构造出不合法容器 —— 必须用真实编码器产物。
0x52, 0x49, 0x46, 0x46, 0x24, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
0x56, 0x50, 0x38, 0x20, 0x18, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x9d,
0x01, 0x2a, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x34, 0x25, 0xa4, 0x00,
0x03, 0x70, 0x00, 0xfe, 0xfb, 0x94, 0x00, 0x00,
}
// ① webp 必须能被解码(否则 memory_blocks 里的 webp 块永远无向量)。
//
// 变异:注释掉 image.go 里的 `_ "golang.org/x/image/webp"` ⇒ 红。
func TestWebpIsDecodable(t *testing.T) {
_, format, err := image.Decode(bytes.NewReader(webpBytes))
if err != nil {
t.Fatalf("webp 解码失败: %v —— image.go 里缺少 _ \"golang.org/x/image/webp\""+
"(线上 4 个 webp 记忆块因此永远拿不到向量)", err)
}
if format != "webp" {
t.Fatalf("格式识别错误: got %q want \"webp\"", format)
}
}
// ② webp 必须能走完整的预处理链路(不只是解码,还要变成 NCHW 张量)。
//
// 只验解码不够:预处理里有缩放/归一化,任何一步对 webp 不适用都算没支持。
func TestWebpPreprocessProducesTensor(t *testing.T) {
const size = 32
mean := []float64{0.48145466, 0.4578275, 0.40821073}
std := []float64{0.26862954, 0.26130258, 0.27577711}
tensor, err := preprocessImage(webpBytes, size, mean, std)
if err != nil {
t.Fatalf("webp 预处理失败: %v", err)
}
if want := 3 * size * size; len(tensor) != want {
t.Fatalf("张量长度 %d,期望 %d(3×%d×%d)", len(tensor), want, size, size)
}
// 1×1 白色图放大后应接近 (1-mean)/std 的白点值;只验非全零,
// 具体像素值由 jpeg/png 的测试覆盖(同一套代码路径)。
nonZero := false
for _, v := range tensor {
if v != 0 {
nonZero = true
break
}
}
if !nonZero {
t.Fatal("张量全为零 —— 预处理没有真正读到像素")
}
}
// ③ png 仍必须可用(防止注册表改动把其它格式弄坏)。
func TestPngStillDecodable(t *testing.T) {
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
img.Set(0, 0, color.RGBA{255, 255, 255, 255})
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
t.Fatalf("构造 png 失败: %v", err)
}
if _, format, err := image.Decode(&buf); err != nil || format != "png" {
t.Fatalf("png 解码异常: format=%q err=%v", format, err)
}
}

View File

@ -1,3 +1,3 @@
module github.com/JianFeeeee/homeagentsdk
module github.com/JianFeeeee/hologramagentsdk
go 1.21.0

View File

@ -1,5 +1,5 @@
-- HomeAgent Lua Plugin SDK
-- Interface contract between Lua plugins and HomeAgent kernel.
-- HologramAgent Lua Plugin SDK
-- Interface contract between Lua plugins and HologramAgent kernel.
-- !impl functions are replaced by Go implementations at runtime.
-- Standalone/debug: pure Lua mock implementations are used.
-- Usage: local sdk = require("sdk")

View File

@ -3,7 +3,7 @@ package sdk
import (
"sync"
"github.com/JianFeeeee/homeagentsdk/meta"
"github.com/JianFeeeee/hologramagentsdk/meta"
)
// SDKVersion 是对外暴露的 SDK 版本号。

View File

@ -6,20 +6,20 @@ import (
"strings"
)
// 反向代理声明:插件告诉 HomeAgent「我起了个 HTTP 服务,请把它反代出去」。
// 反向代理声明:插件告诉 HologramAgent「我起了个 HTTP 服务,请把它反代出去」。
//
// 为什么需要:插件自带 Web UI / HTTP API 时,监听地址在插件自己的配置里
// (如 127.0.0.1:12100),外部无从得知;而 webui 的对外端口通常只有一个
// (默认 :8080,且常经 frp 单端口隧道穿透)。没有声明机制时,用户只能
// 「知道端口 + 自己配转发」,插件换端口就失效。
//
// 设计取舍——**声明式而非注册式**:声明写在 plugin.json 里,由 HomeAgent
// 设计取舍——**声明式而非注册式**:声明写在 plugin.json 里,由 HologramAgent
// 在加载插件时读取聚合,而不是让插件在运行期调 API 注册。理由:
// 1. 静态可发现:未启动/已崩溃的插件,其服务声明依然可见(可给出准确报错
// 「插件 X 声明了 ui 但目标 127.0.0.1:12100 不可达」,而不是静默 404);
// 2. 可版本化:声明随插件包一起分发、可 diff、可审计;
// 3. 旧内核无害:manifest 解析忽略未知字段,未支持该能力的 HomeAgent 读旧
// 插件、或旧 HomeAgent 读新插件都不会报错。
// 3. 旧内核无害:manifest 解析忽略未知字段,未支持该能力的 HologramAgent 读旧
// 插件、或旧 HologramAgent 读新插件都不会报错。
//
// 与 ToolDef / ChannelDef / ConfigDef 同族:SDK 定义声明契约,内核实现行为。
// 声明方式与其它能力一致 —— 在 Start() 里调 RegisterProxy(name, def),
@ -64,7 +64,7 @@ import (
type ProxyDef struct {
// Name 是同一插件内多条声明的唯一标识(如 "ui"、"api")。
// 运行期由 RegisterProxy 的第一个参数填入;声明式由 plugin.json 的
// name 键填入。省略时由 HomeAgent 兜底为 "service"。
// name 键填入。省略时由 HologramAgent 兜底为 "service"。
Name string `json:"name,omitempty"`
// Host 是**子域名标签**(不含基域名),如 "huawei" 对应 huawei.<基域名>。
//
@ -72,12 +72,12 @@ type ProxyDef struct {
// (DNS label 规则)。省略时默认取插件名(下划线转连字符,因为下划线
// 不是合法 DNS label 字符)。
//
// 冲突处理:两个插件声明同一 Host 时,HomeAgent 不做「后者覆盖前者」——
// 冲突处理:两个插件声明同一 Host 时,HologramAgent 不做「后者覆盖前者」——
// 那样会让先声明者静默消失。冲突条目被拒绝并在反代表里记录原因。
Host string `json:"host,omitempty"`
// Target 是上游地址,形如 "127.0.0.1:12100" 或 "http://127.0.0.1:12100"。
// 可带路径前缀(如 "127.0.0.1:3000/base"),HomeAgent 转发时保留该前缀。
// 可带路径前缀(如 "127.0.0.1:3000/base"),HologramAgent 转发时保留该前缀。
//
// 端口由插件自己填它**实际监听**的地址,避免「声明与实际漂移」。
Target string `json:"target"`
@ -135,7 +135,7 @@ type ProxyDef struct {
//
// 为什么做成可声明项:设备网关(remotedevice)这类服务的调用方是**设备**,
// 它们不可能持有浏览器会话 cookie,而服务自身已有接入令牌(如 ws_token)。
// 强制走 HomeAgent 门户鉴权会把这类链路挡死;反过来,插件自带的 UI 若
// 强制走 HologramAgent 门户鉴权会把这类链路挡死;反过来,插件自带的 UI 若
// 声明 none,就等于把管理界面裸露给任何能访问该端口的人。
// 因此必须由插件**逐条**声明,而不是全局一刀切。
Auth string `json:"auth,omitempty"`
@ -198,7 +198,7 @@ func EffectiveProxyAuth(auth string) string {
// ValidProxyHostLabel 校验子域名标签是否合法(DNS label 规则)。
//
// 独立成导出函数:插件作者在写声明时、HomeAgent 在加载时、工具链在打包时
// 独立成导出函数:插件作者在写声明时、HologramAgent 在加载时、工具链在打包时
// 都要用同一套规则判定,避免三处各写一份而互相不一致。
func ValidProxyHostLabel(label string) bool {
if label == "" || len(label) > 63 {
@ -246,7 +246,7 @@ func NormalizeProxyHost(pluginName string) string {
// ValidateProxyDef 校验一条反代声明,返回人类可读的错误说明(合法时为空)。
//
// 为什么要在 SDK 里做校验:HomeAgent 加载插件时必须能明确拒绝坏声明并说明
// 为什么要在 SDK 里做校验:HologramAgent 加载插件时必须能明确拒绝坏声明并说明
// 原因(而不是静默忽略导致用户以为配好了);插件作者也需要在本地就能查出
// 拼错的 Target/Host。同一套规则两端共用。
func ValidateProxyDef(d ProxyDef) string {
@ -338,7 +338,7 @@ func (s *PluginSDK) SetProxyRegistrar(r ProxyRegistrar) {
s.apiMu.Unlock()
}
// RegisterProxy 声明一个需要 HomeAgent 反代出去的服务。
// RegisterProxy 声明一个需要 HologramAgent 反代出去的服务。
//
// 与 RegisterTool / RegisterInputChannel / RegisterOutputChannel 同一风格:
// 显式给名字 + 声明体。名字用于展示、日志与冲突提示(不参与路由 —— 路由键是

View File

@ -6,25 +6,8 @@ import (
"testing"
)
// 存量 plugin.json 里写的是旧值 "homeagent"(本机实测 huawei_smarthome)。
// 改名后必须**继续接受并归一化**它 —— 否则那些插件在加载时被判为非法声明,
// 反代直接不生效,症状是"插件好好的但页面打不开",且没有任何报错指向改名。
func TestProxyAuthLegacyValueStillAccepted(t *testing.T) {
if !ValidProxyAuth(ProxyAuthAgentLegacy) {
t.Fatalf("旧值 %q 必须仍被接受", ProxyAuthAgentLegacy)
}
if ProxyAuthAgentLegacy == ProxyAuthAgent {
t.Fatal("旧值应与新值不同,否则本判据测不到东西")
}
// ★ 归一化是最关键的一步:下游一律只可能拿到新值,
// 否则每个判断点都要写两遍比较,漏一处就是鉴权绕过。
if got := EffectiveProxyAuth(ProxyAuthAgentLegacy); got != ProxyAuthAgent {
t.Fatalf("旧值应归一化为 %q,实际 %q", ProxyAuthAgent, got)
}
}
func TestProxyAuthDefaultsToHomeAgent(t *testing.T) {
// 空串必须归一化为「HomeAgent 统一保护」——这是安全默认。
func TestProxyAuthDefaultsToHologramAgent(t *testing.T) {
// 空串必须归一化为「HologramAgent 统一保护」——这是安全默认。
// 若哪天有人把默认改成 none,这条会立刻红。
if got := EffectiveProxyAuth(""); got != ProxyAuthAgent {
t.Fatalf("空 auth 应归一化为 %q,实际 %q", ProxyAuthAgent, got)
@ -44,6 +27,27 @@ func TestProxyAuthDefaultsToHomeAgent(t *testing.T) {
}
}
// 存量 plugin.json 里写的是旧值 "homeagent"(本机实测 huawei_smarthome)。
// 改名后必须**继续接受并归一化**它 —— 否则那些插件在加载时被判为非法声明,
// 反代直接不生效,症状是"插件好好的但页面打不开",且没有任何报错指向改名。
func TestProxyAuthLegacyValueStillAccepted(t *testing.T) {
if !ValidProxyAuth(ProxyAuthAgentLegacy) {
t.Fatalf("旧值 %q 必须仍被接受", ProxyAuthAgentLegacy)
}
if ProxyAuthAgentLegacy == ProxyAuthAgent {
t.Fatal("旧值应与新值不同,否则本判据测不到东西")
}
// ★ 归一化是最关键的一步:下游一律只可能拿到新值,
// 否则每个判断点都要写两遍比较,漏一处就是鉴权绕过。
if got := EffectiveProxyAuth(ProxyAuthAgentLegacy); got != ProxyAuthAgent {
t.Fatalf("旧值应归一化为 %q,实际 %q", ProxyAuthAgent, got)
}
// 存量 plugin.json 的字面量必须能过校验(这是真实数据,不是假设)。
if !ValidProxyAuth("homeagent") {
t.Fatal(`存量 plugin.json 里的 "auth": "homeagent" 必须仍被接受`)
}
}
func TestValidProxyHostLabel(t *testing.T) {
legit := []string{"huawei", "a", "a-b", "abc123", "0", "x" + string(make([]byte, 0)) + "yz"}
for _, s := range legit {