mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-15 12:15:11 +00:00
上一提交(b6a6168)只替换了 import 路径,**漏了用旧 module 名做逻辑判定 的五处**。CI 当场抓到一处(`TestPrintVersionReportsInjectedVersion`), 但那是测试断言;另外三处是**产品逻辑**,CI 抓不到,是静默失效。 ## 漏改的地方 | 位置 | 原判定 | 失效后果 | |---|---|---| | `cmd_build.go` | `Contains(mod,"homeagent-sdk")` | 认不出 SDK 依赖 ⇒ **不写 replace** | | `cmd_skill.go` | `Contains(gm,"homeagent-sdk")` | 认不出 SDK 仓 ⇒ 不安装 skills | | `yaegi/interp.go` ×2 | `Contains(line,"homeagent-sdk")` | `findSDKGoPath` 找不到 SDK ⇒ 退化到兜底 | 迁移后这些 `Contains` **恒为假**(新名是 `homeagentsdk`,没有连字符)。 三者都**不报错**,只是行为悄悄变成另一条分支——比直接崩更难发现。 ## 关键:不能只认新名 本机实测存量插件工作区的 go.mod **全部仍是旧名**: ``` /home/program/qq-plugin (无 go.mod) /home/program/vanblog-plugin require gitcode.com/JianFeeeee/homeagent-sdk /home/program/mcquery-plugin require gitcode.com/JianFeeeee/homeagent-sdk /home/program/fileproc-plugin require gitcode.com/JianFeeeee/homeagent-sdk /home/program/agentmail/plugins/ require gitcode.com/JianFeeeee/homeagent-sdk ``` 用户已创建的项目不会因为我们改仓就自动更新。所以判定必须 **两种都接受**,而不是把旧名换成新名(那只是把 bug 从"认不出新"换成"认不出旧")。 ## 改动 - `meta` 补 `SDKModule` / `LegacySDKModule` 常量(原注释引用了 `SDKModule` 但常量并不存在——注释先于实现) - 新增 `tools/hmapdev/sdkmodule.go`:`isSDKModule` / `sdkModulePath` / `containsSDKModule` - 新增 `tools/hmapdev/yaegi/sdkmodule.go`:同语义的最小副本 (yaegi 是独立子包,不能 import 父包) - 三处逻辑判定改用上述助手 ## 判据(可杀变异,逐个实测) `sdkmodule_test.go` + `yaegi/sdkmodule_test.go`:覆盖新名/旧名/带版本号的 旧名/空串/核心仓(`HomeAgent`,不是 SDK)/无关模块。 这三个判定点**此前零测试覆盖**——这正是它们能静默失效的原因。 变异验证: - 只认新名 → FAIL「旧名漏认会让存量插件静默走错分支」 - `containsSDKModule` 只认新名 → FAIL「必须认旧名」 ## 验证 - SDK 根 module + hmapdev module:`go build` / `go test` 全通过 - 20 个 example `go vet` 全通过 - `go mod verify` → all modules verified
280 lines
6.9 KiB
Go
280 lines
6.9 KiB
Go
package yaegi
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/traefik/yaegi/interp"
|
|
"github.com/traefik/yaegi/stdlib"
|
|
"github.com/JianFeeeee/homeagentsdk/tools/hmapdev/yaegi/mocksdk"
|
|
)
|
|
|
|
type YaegiDebugger struct {
|
|
Dir string
|
|
PluginName string
|
|
interp *interp.Interpreter
|
|
}
|
|
|
|
func findSDKGoPath(pluginDir string) string {
|
|
// Try to find SDK root from plugin's go.mod replace directive
|
|
gm := filepath.Join(pluginDir, "go.mod")
|
|
if data, err := os.ReadFile(gm); err == nil {
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "replace ") && isSDKModule(line) {
|
|
parts := strings.Fields(line)
|
|
for _, p := range parts {
|
|
if isSDKModule(p) && strings.Contains(p, string(filepath.Separator)) {
|
|
return filepath.Dir(filepath.Dir(p))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Fallback: check common relative locations
|
|
candidates := []string{
|
|
filepath.Join(pluginDir, "..", ".."),
|
|
filepath.Join(pluginDir, "..", "..", ".."),
|
|
}
|
|
for _, c := range candidates {
|
|
abs, _ := filepath.Abs(c)
|
|
if _, err := os.Stat(filepath.Join(abs, "homeagentsdk", "go.mod")); err == nil {
|
|
return abs
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findModuleDir(dir string) string {
|
|
abs, _ := filepath.Abs(dir)
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(abs, "go.mod")); err == nil {
|
|
return abs
|
|
}
|
|
parent := filepath.Dir(abs)
|
|
if parent == abs {
|
|
return ""
|
|
}
|
|
abs = parent
|
|
}
|
|
}
|
|
|
|
func NewYaegiDebugger(dir string, replaces []string) (*YaegiDebugger, error) {
|
|
absDir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve dir: %w", err)
|
|
}
|
|
|
|
goPath := findSDKGoPath(dir)
|
|
// Add replace target directories to GoPath for Yaegi resolution
|
|
for _, r := range replaces {
|
|
_, to, found := strings.Cut(r, "=")
|
|
if !found {
|
|
continue
|
|
}
|
|
to = strings.TrimSpace(to)
|
|
absTo, err := filepath.Abs(to)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
absTo = strings.ReplaceAll(absTo, "\\", "/")
|
|
// Walk up to find module root (contains go.mod)
|
|
modDir := findModuleDir(absTo)
|
|
if modDir != "" {
|
|
parent := filepath.Dir(modDir)
|
|
if goPath == "" {
|
|
goPath = parent
|
|
} else if !strings.Contains(goPath, parent) {
|
|
goPath += string(os.PathListSeparator) + parent
|
|
}
|
|
}
|
|
}
|
|
|
|
i := interp.New(interp.Options{
|
|
GoPath: goPath,
|
|
})
|
|
i.Use(stdlib.Symbols)
|
|
|
|
sdkExports := make(interp.Exports)
|
|
pkg := make(map[string]reflect.Value)
|
|
pkg["New"] = reflect.ValueOf(mocksdk.New)
|
|
pkg["NewPluginSDK"] = reflect.ValueOf(mocksdk.NewPluginSDK)
|
|
pkg["StageOnInput"] = reflect.ValueOf(mocksdk.StageOnInput)
|
|
pkg["StagePreAction"] = reflect.ValueOf(mocksdk.StagePreAction)
|
|
pkg["StagePostAction"] = reflect.ValueOf(mocksdk.StagePostAction)
|
|
pkg["StageBeforeToolcall"] = reflect.ValueOf(mocksdk.StageBeforeToolcall)
|
|
pkg["StageAfterToolcall"] = reflect.ValueOf(mocksdk.StageAfterToolcall)
|
|
pkg["StageBeforeOutput"] = reflect.ValueOf(mocksdk.StageBeforeOutput)
|
|
pkg["StageAfterOutput"] = reflect.ValueOf(mocksdk.StageAfterOutput)
|
|
pkg["StageScopeGlobal"] = reflect.ValueOf(mocksdk.StageScopeGlobal)
|
|
pkg["StageScopeOwnTools"] = reflect.ValueOf(mocksdk.StageScopeOwnTools)
|
|
|
|
typeRegistry := []interface{}{
|
|
(*mocksdk.PluginSDK)(nil),
|
|
(*mocksdk.Plugin)(nil),
|
|
mocksdk.ToolDef{},
|
|
mocksdk.ToolHandler(nil),
|
|
mocksdk.StageHandler(nil),
|
|
(*mocksdk.StageContext)(nil),
|
|
mocksdk.Stage(""),
|
|
mocksdk.ConfigDef{},
|
|
mocksdk.Entity{},
|
|
mocksdk.Relation{},
|
|
mocksdk.Triple{},
|
|
(*mocksdk.Doc)(nil),
|
|
mocksdk.TextEvent{},
|
|
(*mocksdk.Knowledge)(nil),
|
|
(*mocksdk.PersonProfile)(nil),
|
|
mocksdk.SocialRelation{},
|
|
mocksdk.MemItem{},
|
|
mocksdk.ToolCall{},
|
|
mocksdk.ToolResult{},
|
|
(*mocksdk.SettingsAPI)(nil),
|
|
(*mocksdk.MemoryAPI)(nil),
|
|
(*mocksdk.DocMemoryAPI)(nil),
|
|
(*mocksdk.TextMemoryAPI)(nil),
|
|
(*mocksdk.KnowledgeAPI)(nil),
|
|
(*mocksdk.SocialAPI)(nil),
|
|
(*mocksdk.LLMAPI)(nil),
|
|
(*mocksdk.IOInjector)(nil),
|
|
}
|
|
|
|
for _, t := range typeRegistry {
|
|
rt := reflect.TypeOf(t)
|
|
name := rt.Name()
|
|
if name == "" {
|
|
name = rt.Elem().Name()
|
|
}
|
|
pkg[name] = reflect.ValueOf(t)
|
|
}
|
|
|
|
sdkExports["github.com/JianFeeeee/homeagentsdk/sdk"] = pkg
|
|
i.Use(sdkExports)
|
|
|
|
return &YaegiDebugger{
|
|
Dir: absDir,
|
|
PluginName: filepath.Base(absDir),
|
|
interp: i,
|
|
}, nil
|
|
}
|
|
|
|
func (d *YaegiDebugger) LoadPlugin() error {
|
|
entries, err := os.ReadDir(d.Dir)
|
|
if err != nil {
|
|
return fmt.Errorf("read dir: %w", err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
|
|
continue
|
|
}
|
|
if strings.HasSuffix(entry.Name(), "_test.go") {
|
|
continue
|
|
}
|
|
if entry.Name() == "debug_main.go" {
|
|
continue
|
|
}
|
|
|
|
src, err := os.ReadFile(filepath.Join(d.Dir, entry.Name()))
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", entry.Name(), err)
|
|
}
|
|
|
|
_, err = d.interp.Eval(string(src))
|
|
if err != nil {
|
|
return fmt.Errorf("eval %s: %w", entry.Name(), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *YaegiDebugger) HasNewPluginFactory() (bool, error) {
|
|
fset := token.NewFileSet()
|
|
pkgs, err := parser.ParseDir(fset, d.Dir, nil, 0)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, pkg := range pkgs {
|
|
for _, f := range pkg.Files {
|
|
for _, decl := range f.Decls {
|
|
if fn, ok := decl.(*ast.FuncDecl); ok && fn.Name.Name == "NewPluginFactory" {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (d *YaegiDebugger) StartREPL() error {
|
|
hasFactory, _ := d.HasNewPluginFactory()
|
|
|
|
if hasFactory {
|
|
v, err := d.interp.Eval(fmt.Sprintf(`NewPluginFactory("%s", nil)`, d.PluginName))
|
|
if err != nil {
|
|
return fmt.Errorf("call NewPluginFactory: %w", err)
|
|
}
|
|
|
|
plugin := v.Interface().(mocksdk.Plugin)
|
|
fmt.Printf("[debug] Plugin: %s\n", plugin.Name())
|
|
|
|
sdk := mocksdk.New(d.PluginName)
|
|
if err := plugin.Start(sdk); err != nil {
|
|
return fmt.Errorf("plugin.Start: %w", err)
|
|
}
|
|
fmt.Printf("[debug] Plugin started. Registered %d tools.\n", len(sdk.ListTools()))
|
|
for _, def := range sdk.ListTools() {
|
|
fmt.Printf(" - %s: %s\n", def.Name, def.Description)
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("=== Yaegi REPL ===")
|
|
fmt.Println("Type Go expressions, 'tools' to list, 'call <name> <json>' to invoke, 'exit' to quit.")
|
|
fmt.Println()
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
for {
|
|
fmt.Print("> ")
|
|
if !scanner.Scan() {
|
|
break
|
|
}
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if line == "exit" || line == "quit" || line == "q" {
|
|
break
|
|
}
|
|
if line == "tools" {
|
|
if hasFactory {
|
|
v, _ := d.interp.Eval(fmt.Sprintf(`NewPluginFactory("%s", nil)`, d.PluginName))
|
|
plugin := v.Interface().(mocksdk.Plugin)
|
|
sdk := mocksdk.New(d.PluginName)
|
|
plugin.Start(sdk)
|
|
for _, def := range sdk.ListTools() {
|
|
fmt.Printf(" %s: %s\n", def.Name, def.Description)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
v, err := d.interp.Eval(line)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
} else if v.IsValid() && v.CanInterface() {
|
|
result := v.Interface()
|
|
fmt.Printf("%+v\n", result)
|
|
}
|
|
}
|
|
return nil
|
|
}
|