Files
homeagent-sdk/tools/hmapdev/yaegi/interp.go
JianFeeeee b6a6168e65 chore(module)!: module 路径随托管地迁移 gitcode → github
SDK 仓已迁到 GitHub 作为权威源,go module 路径同步:
`gitcode.com/JianFeeeee/homeagent-sdk` → `github.com/JianFeeeee/homeagentsdk`

## 范围

74 个文件:71 个 Go 源码 + go.mod/go.sum + hmapdev 模板 + CI 与打包脚本。

主仓(HomeAgent)侧对应改动是 6d38a164。本仓这些文件当时**不在工作树上**
(见下),所以那次的批量替换扫不到它们——这里是补偿。

## 为何这批文件当时不在工作树上(事故与恢复)

10-08 07:58 主仓做 checkout/cherry-pick 序列时,把本仓工作树的 20 个文件
误清理了(`example/*/plugin.go` + `plg.json`,10 个示例)。

根因是**结构误判**:本仓是「一目录两仓库」结构下主仓 `third_party/` 里的
**独立 git 仓库**,而主仓 .gitignore 忽略了 `example/`,于是它在主仓
`git status` 里不可见 —— 看着像「不受跟踪的构建产物」,实际是本仓受跟踪的源码。

已全部恢复(`git checkout -- <D 文件>`,只恢复删除、不动迁移改动),
20 个文件与 HEAD 逐字节一致。

**破坏的连锁后果**:恢复出来的 `plugin.go` 仍写着旧 module 路径,
因为迁移时它们已不在工作树上。所以这次补偿是必须的,不是可选优化。

## 保留 gitcode 引用

以下位置的 `gitcode` **有意保留**,不要"顺手清掉":
- README.md / docs 里「二进制实际分发地址目前是 gitcode」的说明
- mkdocs.yml 的 gitcode 镜像链接(国内直连更快)
- `tools/hmapdev/templates.go` 里「SDK 已从 gitcode 迁到 GitHub」的注释

gitcode 仍是**只读镜像**(源码同步;release 附件暂时仍在那边)。

## 验证

- 本仓根 module:`go build ./...` + `go test ./...` 全通过
- 全部 20 个 example:`go vet` 通过(补偿前 10 个因旧 module 路径失败)
- example/qq:`go test` 通过,且能从源码 build 出可用 plugin 产物
2026-10-09 07:25:25 +08:00

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 ") && strings.Contains(line, "homeagent-sdk") {
parts := strings.Fields(line)
for _, p := range parts {
if strings.Contains(p, "homeagent-sdk") && 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
}