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 保留。
153 lines
5.7 KiB
Go
153 lines
5.7 KiB
Go
package core
|
||
|
||
import (
|
||
"testing"
|
||
|
||
agentAPI "github.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
agentIO "github.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
sdk "github.com/JianFeeeee/HomeAgent/internal/sdk"
|
||
)
|
||
|
||
// 这组判据针对一个「发了但没人收到」的缺陷:
|
||
//
|
||
// agent 自己在 stepLLM 里把用量记进 usageLedger,也发进了
|
||
// EventAgentLLMChain 事件;但同步注入方(WebUI 的 /v1/chat/completions、
|
||
// cli.sock、clawhubadapter)拿到的回包来自 emitResponse,而 emitResponse
|
||
// **新建**了一个 StageCtx,只从插件侧读 TokenUsage。
|
||
// agent 自己算出来的用量从未传进去 ⇒ 回包 payload 里没有 usage。
|
||
//
|
||
// 实测证据(部署实例 8080):
|
||
//
|
||
// curl /v1/chat/completions ... → 回包中 "usage" 字段不存在(回复正常)
|
||
//
|
||
// ★ 还有一个必须一并守住的口径(本项目已踩过一次):
|
||
// handler_openai.go 用 `Payload["usage"].(map[string]interface{})` 取值,
|
||
// 而 Go 的 map 类型断言是**精确匹配** —— 给 map[string]int 会断言失败,
|
||
// 于是 usage 静默变 nil,两边都不报错。所以回包里的 usage 必须是
|
||
// map[string]interface{},判据就按消费方的要求断言动态类型。
|
||
|
||
// newEmitAgent 造一个只够跑 emitResponse 的 Agent(照 agent_tools_test.go 的直构法)。
|
||
func newEmitAgent() *Agent {
|
||
return &Agent{stageHost: NewStageHost()}
|
||
}
|
||
|
||
// asUsageMap 取出回包里的 usage 并要求它是 map[string]interface{}。
|
||
//
|
||
// 断言**动态类型**而不是只断言值,是因为消费方(handler_openai.go)用的就是
|
||
// 精确类型断言:这里是唯一能拦住「map[string]int 静默变 nil」的地方。
|
||
func asUsageMap(t *testing.T, v interface{}) map[string]interface{} {
|
||
t.Helper()
|
||
m, ok := v.(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("usage 动态类型=%T,期望 map[string]interface{}(消费方用精确断言,"+
|
||
"给 map[string]int 会让它在回包里静默变 nil)", v)
|
||
}
|
||
return m
|
||
}
|
||
|
||
func tk(prompt, completion, total, cacheRead, cacheMiss, reasoning int, cacheReported bool) agentAPI.TokenUsage {
|
||
return agentAPI.TokenUsage{
|
||
Prompt: prompt, Completion: completion, Total: total,
|
||
CacheRead: cacheRead, CacheMiss: cacheMiss,
|
||
ReasoningTokens: reasoning, CacheReported: cacheReported,
|
||
}
|
||
}
|
||
|
||
// TestEmitResponseCarriesSessionUsage 是核心判据:
|
||
// 会话里有过 LLM 记账之后,同步回执必须带上 usage,
|
||
// 且数字来自 agent 自己的累计(不是零值占位)。
|
||
func TestEmitResponseCarriesSessionUsage(t *testing.T) {
|
||
a := newEmitAgent()
|
||
// 两次 LLM 调用:一次报了缓存,一次没报(分母口径见 usagestats.go)。
|
||
a.usageLedger.record(tk(1000, 200, 1200, 768, 232, 50, true))
|
||
a.usageLedger.record(tk(500, 100, 600, 0, 0, 0, false))
|
||
ch := make(chan *agentIO.OutputEvent, 1)
|
||
evt := &agentIO.InputEvent{
|
||
RequestID: "req-1", Source: "http", OutputChannel: "http", ResponseCh: ch,
|
||
}
|
||
sum := agentAPI.TokenUsage{}
|
||
sum.Add(tk(1000, 200, 1200, 768, 232, 50, true))
|
||
sum.Add(tk(500, 100, 600, 0, 0, 0, false))
|
||
a.emitResponse(evt, "好的", sum)
|
||
|
||
select {
|
||
case out := <-ch:
|
||
if out == nil {
|
||
t.Fatal("回执为 nil")
|
||
}
|
||
raw, ok := out.Payload["usage"]
|
||
if !ok || raw == nil {
|
||
t.Fatalf("同步回执缺少 usage(这正是 /v1/chat/completions 取不到的根因);payload=%v", out.Payload)
|
||
}
|
||
m := asUsageMap(t, raw)
|
||
// 会话累计:prompt 1000+500、completion 200+100、total 1200+600
|
||
for _, c := range []struct {
|
||
key string
|
||
want int
|
||
}{
|
||
{"prompt_tokens", 1500},
|
||
{"completion_tokens", 300},
|
||
{"total_tokens", 1800},
|
||
{"cache_read_tokens", 768},
|
||
{"cache_miss_tokens", 232},
|
||
{"reasoning_tokens", 50},
|
||
} {
|
||
if got, _ := m[c.key].(int); got != c.want {
|
||
t.Errorf("%s=%v,期望 %v(会话累计)", c.key, m[c.key], c.want)
|
||
}
|
||
}
|
||
default:
|
||
t.Fatal("emitResponse 没有写回执(不变量 I5 被破坏)")
|
||
}
|
||
}
|
||
|
||
// TestEmitResponseOmitsUsageWhenNothingRecorded 是上一条的边界:
|
||
// 一次 LLM 都没跑过时不要凭空造 usage —— 否则消费方会把「没有数据」
|
||
// 当成「用了 0 token」,与本项目既有的 CacheHitRate ok=false 口径一致。
|
||
func TestEmitResponseOmitsUsageWhenNothingRecorded(t *testing.T) {
|
||
a := newEmitAgent()
|
||
ch := make(chan *agentIO.OutputEvent, 1)
|
||
evt := &agentIO.InputEvent{
|
||
RequestID: "req-2", Source: "http", OutputChannel: "http", ResponseCh: ch,
|
||
}
|
||
a.emitResponse(evt, "你好", agentAPI.TokenUsage{})
|
||
|
||
select {
|
||
case out := <-ch:
|
||
if u, ok := out.Payload["usage"]; ok && u != nil {
|
||
t.Errorf("没有任何 LLM 调用时报了 usage=%v;应当缺省,让消费方显示「—」", u)
|
||
}
|
||
default:
|
||
t.Fatal("没有写回执")
|
||
}
|
||
}
|
||
|
||
// TestEmitResponsePluginUsageStillHonored 保住既有契约:
|
||
// 插件(StageBeforeOutput)显式设置的 TokenUsage 必须仍然生效 ——
|
||
// 这是本次改动前唯一能拿到 usage 的路径,不能因为补了 agent 侧就丢掉。
|
||
func TestEmitResponsePluginUsageStillHonored(t *testing.T) {
|
||
a := newEmitAgent()
|
||
a.stageHost.RegisterStage(sdk.StageBeforeOutput, func(ctx *sdk.StageContext) error {
|
||
ctx.TokenUsage = map[string]int{"prompt_tokens": 7, "completion_tokens": 3, "total_tokens": 10}
|
||
return nil
|
||
})
|
||
|
||
ch := make(chan *agentIO.OutputEvent, 1)
|
||
evt := &agentIO.InputEvent{RequestID: "req-3", Source: "http", OutputChannel: "http", ResponseCh: ch}
|
||
a.emitResponse(evt, "hi", agentAPI.TokenUsage{})
|
||
|
||
select {
|
||
case out := <-ch:
|
||
raw, ok := out.Payload["usage"]
|
||
if !ok || raw == nil {
|
||
t.Fatalf("插件显式设置的 usage 丢了;payload=%v", out.Payload)
|
||
}
|
||
m := asUsageMap(t, raw)
|
||
if got, _ := m["prompt_tokens"].(int); got != 7 {
|
||
t.Errorf("prompt_tokens=%v,期望插件设置的 7", m["prompt_tokens"])
|
||
}
|
||
default:
|
||
t.Fatal("没有写回执")
|
||
}
|
||
}
|