mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
## 背景 仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑), gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。 但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源 不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。 ## 关键区分(这是本次改动安全的前提) 两个 module 是**不同**的,影响面差一个数量级: | module | 谁依赖 | 改它的后果 | |---|---|---| | `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 | | `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 | ★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0), 只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串** (`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。 ⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。 SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配, 否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。 ## 改动 - `go.mod` / `go.sum`:两个 module 路径 + require/replace - 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径 - SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板 - 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等) ## 明确**保留** gitcode 的地方(镜像机制,不能动) - `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job) - `gitcode.com/api/v5/...`(release 附件上传) - git remote `gitcode`(镜像拉取/推送) - `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维) ## 验证 - `go build ./...` / `go vet ./...` 绿 - `go test ./... -count=1` → 48 包 ok / 0 FAIL - hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`) - 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0 - 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好 ## 顺带修正 `hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」—— module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**: 离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
139 lines
4.6 KiB
Go
139 lines
4.6 KiB
Go
// 结构性判据:多模态工具**已从内核退场**(2026-10-06)
|
||
//
|
||
// ## 为什么需要结构性判据
|
||
//
|
||
// 前面 plugin_test.go 验的是「插件侧行为对不对」。但这次改动的**要害**
|
||
// 不是插件能不能用,而是**内核不再持有这三个工具** ——
|
||
// 因为内核承诺零 IO,而它们要读本地文件。
|
||
//
|
||
// 行为判据抓不住这件事:哪怕把内核实现改回去,只要插件侧仍然工作,
|
||
// 功能测试照样全绿。而「内核能不能读文件」正是本仓第一原则的红线。
|
||
//
|
||
// ⇒ 这条判据直接扫源码:三个工具名不得出现在 internal/agent/core/ 的
|
||
//
|
||
// 实现文件里,也不得有 os.ReadFile 之类为它们服务。
|
||
package core
|
||
|
||
import (
|
||
"os"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// mediaToolNames 是已移出内核的三个工具。
|
||
var mediaToolNames = []string{"describe_image", "ocr_image", "transcribe_audio"}
|
||
|
||
// kernelImplFiles 内核的实现文件(不含 _test.go —— 判据自身要断言的东西不该自己触发)。
|
||
func kernelImplFiles(t *testing.T) []string {
|
||
t.Helper()
|
||
ents, err := os.ReadDir(".")
|
||
if err != nil {
|
||
t.Fatalf("读包目录: %v", err)
|
||
}
|
||
var out []string
|
||
for _, e := range ents {
|
||
n := e.Name()
|
||
if strings.HasSuffix(n, "_test.go") || !strings.HasSuffix(n, ".go") {
|
||
continue
|
||
}
|
||
out = append(out, n)
|
||
}
|
||
if len(out) < 5 {
|
||
t.Fatalf("只找到 %d 个实现文件,判据覆盖不足", len(out))
|
||
}
|
||
return out
|
||
}
|
||
|
||
// TestMediaToolsGoneFromKernel 钉住:内核实现里不再出现这三个工具名。
|
||
//
|
||
// ★ 这是本次改动的**核心不变量**。它对应 README 的第一原则:
|
||
//
|
||
// 内核零 IO,而这三个工具要读本地文件。
|
||
func TestMediaToolsGoneFromKernel(t *testing.T) {
|
||
for _, f := range kernelImplFiles(t) {
|
||
b, err := os.ReadFile(f)
|
||
if err != nil {
|
||
t.Fatalf("读 %s: %v", f, err)
|
||
}
|
||
src := string(b)
|
||
// 只看代码行:注释里可以说明「已移出」(那是给人看的)
|
||
for _, line := range strings.Split(src, "\n") {
|
||
trimmed := strings.TrimSpace(line)
|
||
if strings.HasPrefix(trimmed, "//") {
|
||
continue
|
||
}
|
||
for _, name := range mediaToolNames {
|
||
if !strings.Contains(line, name) {
|
||
continue
|
||
}
|
||
// 允许出现在「已移出」的说明性注释里(带 ★ 或 已移出 标记)
|
||
if strings.Contains(line, "已移出") || strings.Contains(line, "★") {
|
||
continue
|
||
}
|
||
t.Errorf("internal/agent/core/%s 仍出现 %q —— 它已移出为内置插件 multimodal:\n\t%s",
|
||
f, name, trimmed)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestKernelHasNoMediaFileReading 钉住:内核不得为多模态读本地文件。
|
||
//
|
||
// ★ 比「名字没出现」更强:任何人新写一个 os.ReadFile 来干同样的事,
|
||
//
|
||
// 这条也会红。
|
||
//
|
||
// 已知例外:context.go 的上下文窗口持久化(savePath)属**记忆子系统**,
|
||
// 已在允许清单里 —— 它不是多模态,且属于内核既有的职责。
|
||
func TestKernelHasNoMediaFileReading(t *testing.T) {
|
||
// 允许的内核磁盘读:上下文窗口持久化(记忆面)
|
||
allowed := map[string]bool{
|
||
"context.go": true, // RelevanceContext.savePath 的 load/save
|
||
}
|
||
readFile := regexp.MustCompile(`os\.(ReadFile|Open)\(`)
|
||
for _, f := range kernelImplFiles(t) {
|
||
if allowed[f] {
|
||
continue
|
||
}
|
||
b, err := os.ReadFile(f)
|
||
if err != nil {
|
||
t.Fatalf("读 %s: %v", f, err)
|
||
}
|
||
for i, line := range strings.Split(string(b), "\n") {
|
||
if readFile.MatchString(line) && !strings.HasPrefix(strings.TrimSpace(line), "//") {
|
||
t.Errorf("internal/agent/core/%s:%d 出现磁盘读取 %q —— "+
|
||
"内核承诺零 IO(多模态读文件已移到内置插件 multimodal)",
|
||
f, i+1, strings.TrimSpace(line))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestMediaToolsRegisteredByPlugin 确认能力没丢:插件侧确实注册了这三个工具。
|
||
//
|
||
// ★ 与上一条成对:「内核不该有」+「插件要有」= 迁移而不是删除。
|
||
//
|
||
// 只断言前者会放过「直接删掉功能」这种退化。
|
||
func TestMediaToolsRegisteredByPlugin(t *testing.T) {
|
||
// 插件侧的注册清单在 internal/plugins/multimodal/plugin.go。
|
||
// 这里只做**跨包引用检查**(core 包不该 import multimodal,那是反向依赖)。
|
||
if strings.Contains(kernelSourceDump(t), `"github.com/JianFeeeee/HomeAgent/internal/plugins/multimodal"`) {
|
||
t.Error("core 反向 import 了 multimodal 插件 —— " +
|
||
"工具由插件注册进 StageHost,核心不应知道插件实现")
|
||
}
|
||
}
|
||
|
||
func kernelSourceDump(t *testing.T) string {
|
||
t.Helper()
|
||
var sb strings.Builder
|
||
for _, f := range kernelImplFiles(t) {
|
||
b, err := os.ReadFile(f)
|
||
if err != nil {
|
||
t.Fatalf("读 %s: %v", f, err)
|
||
}
|
||
sb.Write(b)
|
||
}
|
||
return sb.String()
|
||
}
|