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 保留。
119 lines
4.8 KiB
Go
119 lines
4.8 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||
)
|
||
|
||
// 阶段 B:内置工具注册进 ToolAPI 面。
|
||
//
|
||
// 背景(真机实跑抓到的架构缺口):`memory_*` / `knowledge_*` / `doc_*` /
|
||
// `person_*` 这 20+ 个是**内核内置**工具,在 executeToolCallInner 里按
|
||
// **前缀分派**(toolcall.go:96 等),**从不注册进 ToolAPI** —— 而 ToolAPI
|
||
// 只含 StageHost 插件工具与 IO 设备工具。
|
||
// ⇒ 任何经 ToolAPI 的调用方(本仓的 seq 插件)既查不到、也调不了内置工具。
|
||
// 实测现象:seq_run 报「工具 knowledge_list 不存在或未注册」,而同一轮
|
||
// 模型直接调 knowledge_list 是**成功**的。
|
||
//
|
||
// 关键前提(已核实,勿推翻):模型看到的工具来自 buildToolDefs,它走
|
||
// `a.io.GetAllTools()` / `a.stageHost.GetToolDefs()` / `a.indexer…` **直调**,
|
||
// 与 toolImpl(只经 PluginSDK.Tool() 暴露给插件)是**两条不重叠的路径**。
|
||
// ⇒ 把内置工具加进 ToolAPI 不会让模型看到重复工具。
|
||
|
||
// ① 内置工具必须能从 ToolAPI 查到(查不到 ⇒ 插件无法编排它们)。
|
||
func TestBuiltinToolsVisibleViaToolAPI(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
api := toolAPIOf(t, a)
|
||
|
||
// 这些是 buildToolDefs 里由 a.memory/a.knowledge 等门控的内置工具。
|
||
// 本用例的 agent 未接 memory/knowledge,故用**无条件**注册的那批:
|
||
// output_list_channels / input_channels / get_plugin_tools / plgreload 等。
|
||
for _, name := range []string{
|
||
"output_list_channels", "input_channels", "get_plugin_tools",
|
||
} {
|
||
if api.ToolDefByName(name) == nil {
|
||
t.Errorf("内置工具 %q 在 ToolAPI 上查不到 —— 插件无法编排它", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ② ★ ToolAPI 上查到 ≠ 能调通。必须真的能执行。
|
||
func TestBuiltinToolExecutableViaToolAPI(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
api := toolAPIOf(t, a)
|
||
|
||
res, err := api.ExecuteTool("output_list_channels", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("经 ToolAPI 执行内置工具失败: %v", err)
|
||
}
|
||
if res == nil {
|
||
t.Error("执行成功但结果为 nil")
|
||
}
|
||
}
|
||
|
||
// ③ ★ 门控语义必须保持:未接 memory 时 memory_* 不该出现在 ToolAPI 上。
|
||
//
|
||
// 内置工具定义是由运行期状态门控的(`if a.memory != nil` 等)。若注册面
|
||
// 不看状态地全量暴露,会让轻量子(memory==nil)声称自己能操作记忆。
|
||
func TestBuiltinToolGatingRespected(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider()) // 无 memory / knowledge
|
||
api := toolAPIOf(t, a)
|
||
|
||
if api.ToolDefByName("memory_merge") != nil {
|
||
t.Error("未接 memory 却声称有 memory_merge —— 门控语义被破坏")
|
||
}
|
||
if api.ToolDefByName("knowledge_list") != nil {
|
||
t.Error("未接 knowledge 却声称有 knowledge_list —— 门控语义被破坏")
|
||
}
|
||
}
|
||
|
||
// ④ ★ 接了 memory/knowledge 时必须**可见**(这是本次要补的缺口)。
|
||
func TestBuiltinToolVisibleWhenSubsystemPresent(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
a.knowledge = knowledge.NewStore(t.TempDir()) // 打开 knowledge 门控
|
||
api := toolAPIOf(t, a)
|
||
|
||
if api.ToolDefByName("knowledge_list") == nil {
|
||
t.Error("接了 knowledge 但 ToolAPI 上查不到 knowledge_list —— 缺口未补")
|
||
}
|
||
}
|
||
|
||
// ⑤ ★ 参数校验也要走同一条路:经 ToolAPI 调用同样受 schema 预校验。
|
||
//
|
||
// 否则插件能用 ToolAPI 绕过 1c 的校验(缺 required 却不报错)。
|
||
func TestBuiltinToolViaToolAPIStillValidated(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
api := toolAPIOf(t, a)
|
||
|
||
// output_list_channels 无 required 参数 ⇒ 用一个确定有 required 的:
|
||
// persona_set 在 personaStore 为 nil 时不可见,故用 output_send__ 家族的帮助工具。
|
||
// 这里退一步验证更本质的一点:ToolAPI 路径上**没有**旁路校验。
|
||
// 用一个不存在的工具名验证"不存在"语义。
|
||
_, err := api.ExecuteTool("definitely_not_a_tool", map[string]interface{}{})
|
||
if err == nil {
|
||
t.Error("调用不存在的工具却成功了 —— ToolAPI 缺少存在性判定")
|
||
}
|
||
if !strings.Contains(strings.ToLower(err.Error()), "not found") &&
|
||
!strings.Contains(err.Error(), "不存在") {
|
||
t.Errorf("错误信息应明示『不存在』,实际: %v", err)
|
||
}
|
||
}
|
||
|
||
// ⑥ 内置工具**不声明**并发安全:它们含 SQLite 写与召回,且门控依赖 agent 状态。
|
||
func TestBuiltinToolsNotParallelSafeByDefault(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
api := toolAPIOf(t, a)
|
||
|
||
for _, name := range []string{"output_list_channels", "input_channels", "get_plugin_tools"} {
|
||
def := api.ToolDefByName(name)
|
||
if def == nil {
|
||
continue
|
||
}
|
||
if def.ParallelSafe {
|
||
t.Errorf("内置工具 %q 默认声明了 ParallelSafe —— 写类工具被并发执行有风险", name)
|
||
}
|
||
}
|
||
}
|