refactor(toolchain)!: 工具链 plugindev 更名为 hmapdev,module path 改回 gitcode

- 目录 tools/plugindev → tools/hmapdev,可执行文件名/平台产物名同步
  (hmapdev_linux_amd64 等;包格式仍叫 .hmap)
- module path github.com/JianFeeeee/homeagent-sdk/tools/... → gitcode.com/...
  (与仓库实际托管一致;核心仓不依赖该 path,改动无外部影响)
- SDK 存储目录 ~/.homeagent/plugindev/sdk → ~/.homeagent/hmapdev/sdk
  新目录不存在而旧目录存在时沿用旧目录 → 已装 SDK 版本不会丢失
- 命令表/usage/--help/生成项目 README/示例 README/NSIS 安装器/
  package/build.sh/build-examples.sh 全部同步;PLUGINDEV 环境变量保留兼容
- sdk/ 目录零改动(公开接口不变)

验证:
- go build ./... ok;go test ./tools/hmapdev/ ok(含模板接线守卫 TestProcTemplate_CoversAllCoreMethods)
- bash -n package/{build,build-examples}.sh ok
- 端到端:hmapdev init demo && hmapdev build → dist/demo_bundle.hmap(linux+darwin)
- 本机安装 /usr/local/bin/hmapdev,旧名以软链保留;sdk list/current 正常
This commit is contained in:
JianFeeeee
2026-09-12 12:30:20 +08:00
parent 69ff3089a4
commit b237787c90
33 changed files with 135 additions and 122 deletions

View File

@ -0,0 +1,24 @@
# {{.Plg.Name}}
{{.Plg.Description}}
## Build
```bash
plugindev build
```
## Install
Upload the `.hmap` file through the Plugin Manager API:
```bash
curl -X POST http://localhost:8080/api/v1/plugins \
-H "Content-Type: application/octet-stream" \
--data-binary @dist/<name_en_snake>_linux_amd64.hmap
```
## Lifecycle
- `RegisterStopHandler` — runs on every stop (including reload/disable), before `Stop()`.
- `RegisterOnRemoveHandler` — runs **only on uninstall (remove)**, after `Stop()`; clean up the plugin's own data files here. Reload/disable do NOT trigger it. See the onRemove demo in `main.go`.

View File

@ -0,0 +1,61 @@
package main
import (
"encoding/json"
"fmt"
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
type Plugin struct {
name string
sdk *sdk.PluginSDK
}
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
s.RegisterStopHandler(func() {
fmt.Printf("[%s] stop handler running\n", p.name)
})
s.RegisterOnRemoveHandler(func() {
fmt.Printf("[%s] onRemove handler running\n", p.name)
})
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "plugin.{{.Plg.Name}}.example",
Default: "hello",
Type: "string",
DisplayName: "示例配置",
Description: "An example configuration key",
Category: "{{.Plg.Name}}",
})
tp := p.name + "_"
s.RegisterTool(tp+"hello", sdk.ToolDef{
Name: tp + "hello",
Description: "A hello world tool",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
}, p.handleHello)
fmt.Printf("[%s] started\n", p.name)
return nil
}
func (p *Plugin) Stop() error {
fmt.Printf("[%s] stopped\n", p.name)
return nil
}
func (p *Plugin) handleHello(args map[string]interface{}) (interface{}, error) {
return map[string]interface{}{
"content": "Hello from {{.Plg.Name}} plugin!",
}, nil
}

View File

@ -0,0 +1,45 @@
local plugin = {
name = "{{.Plg.Name}}",
tools = {},
stages = {},
settings = {}
}
function plugin:start(sdk)
sdk.log("{{.Plg.Name}} plugin starting...")
-- Register a configuration setting
-- sdk.settings.register({
-- key = "{{.Plg.Name}}.example",
-- default = "hello",
-- type = "string",
-- display_name = "Example Config",
-- description = "An example configuration key"
-- })
-- Register a tool
local ok = sdk:register_tool("{{.Plg.Name}}_hello", {
name = "{{.Plg.Name}}_hello",
description = "A hello world tool",
parameters = {
type = "object",
properties = {}
}
}, function(args)
return { content = "Hello from {{.Plg.Name}} plugin!" }
end)
if not ok then
sdk.log("error: failed to register tool")
return false
end
sdk.log("{{.Plg.Name}} plugin started")
return true
end
function plugin:stop()
return true
end
return plugin

View File

@ -0,0 +1,11 @@
{
"name": "{{.Plg.Name}}",
"name_zh": "{{.Plg.NameZh}}",
"name_en": "{{.Plg.NameEn}}",
"version": "{{.Plg.Version}}",
"description": "{{.Plg.Description}}",
"author": "{{.Plg.Author}}",
"entry": "{{.Plg.Entry}}",
"tags": {{.Plg.Tags}},
"targets": "{{.Plg.Targets}}"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
//go:build linux || darwin || freebsd
package main
import (
"fmt"
"os"
"syscall"
)
// Unix 侧共享段挂载:内核经 ExtraFiles 传入继承的 fd。
//
// 统一共享内存区域布局§13.1
//
// fd 3 = 统一区域SuperBlock + StageContext + EvtRing
// fd 4 = 事件通知Linux eventfd / macOS pipe 读端)
//
// 继承的 fd 无需文件名,也不残留——这是选 memfd 而非 /dev/shm 的原因。
const (
fdUnifiedShm = 3
fdEvtNotifier = 4
)
// attachUnifiedShm 挂载统一共享内存区域。
//
// 各进程 mmap 到不同虚拟地址,段内一律用相对偏移而非指针,故仍能正确解引用
// (实验 2 已验证父子 mmap 基址不同时偏移解引用正确)。
func attachUnifiedShm(size int) ([]byte, error) {
return syscall.Mmap(fdUnifiedShm, 0, size,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
}
// openEvtNotifier 打开事件通知读端。
func openEvtNotifier() (evtWaiter, error) {
f := os.NewFile(fdEvtNotifier, "evtnotify")
if f == nil {
return nil, fmt.Errorf("fd %d 不是有效的通知句柄", fdEvtNotifier)
}
return &unixEvtWaiter{f: f}, nil
}
// unixEvtWaiter 用 eventfd/pipe 的阻塞 Read 等待通知。
//
// os.NewFile 把 fd 注册进 runtime netpollerRead 阻塞时只 park goroutine
// 不占 OS 线程(实验 1200 个等待者仅增 1 个 OS 线程)。
// 反面对照是经 cgo 调 sem_wait——那会阻塞整个 M。
type unixEvtWaiter struct {
f *os.File
}
func (w *unixEvtWaiter) Wait(buf []byte) error {
_, err := w.f.Read(buf)
return err
}

View File

@ -0,0 +1,153 @@
//go:build windows
package main
import (
"fmt"
"os"
"syscall"
"unsafe"
)
// Windows 侧共享段挂载:走命名对象而非继承 fd。
//
// 为何不能照抄 UnixWindows 没有 fd 继承语义,`ExtraFiles` 在 os/exec 的
// Windows 实现里不被支持。等价机制是命名内核对象——父进程用
// CreateFileMapping / CreateEvent 建带名字的对象,子进程按同名 Open 拿到同一对象。
//
// 名字经环境变量传入(内核 internal/plugin/proc/plugin_windows.go 设置),
// 而不是硬编码:多个 homed 实例并存时不能撞名。
//
// **这是 §9.2 的正解**C ABI 时代 Windows 是第三套独立 ABI 实现,
// stage 只下发 3 个字段且完全没有写回sanitizer 这类改写型插件静默失效。
// 现在 Windows 与 Unix 共用同一份 RPC 逻辑与同一份共享段布局,
// 差异被收敛到本文件的三个函数里。
const (
envStageShmName = "HOMEAGENT_SHM_STAGE"
envEvtRingName = "HOMEAGENT_SHM_EVTRING"
envEvtEventName = "HOMEAGENT_EVT_EVENT"
)
// Windows API 绑定:用 LazyDLL 而非 golang.org/x/sys/windows。
//
// 原因OpenFileMappingW / OpenEventW 未被标准库 syscall 包导出。
// 引入 x/sys 会给**每个插件的 go.mod 加一个新依赖**
// 而「外部插件零改动」是本次迁移的硬约束(插件仅依赖公开 SDK
// LazyDLL 属于标准库 syscall零新增依赖。
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procOpenFileMappingW = kernel32.NewProc("OpenFileMappingW")
procOpenEventW = kernel32.NewProc("OpenEventW")
)
const (
winEventModifyState = 0x0002
winSynchronize = 0x00100000
)
// openFileMappingW 封装 OpenFileMappingW。
func openFileMappingW(access uint32, inherit bool, name *uint16) (syscall.Handle, error) {
var inheritFlag uintptr
if inherit {
inheritFlag = 1
}
r, _, err := procOpenFileMappingW.Call(
uintptr(access), inheritFlag, uintptr(unsafe.Pointer(name)))
if r == 0 {
return 0, err
}
return syscall.Handle(r), nil
}
// openEventW 封装 OpenEventW。
func openEventW(access uint32, inherit bool, name *uint16) (syscall.Handle, error) {
var inheritFlag uintptr
if inherit {
inheritFlag = 1
}
r, _, err := procOpenEventW.Call(
uintptr(access), inheritFlag, uintptr(unsafe.Pointer(name)))
if r == 0 {
return 0, err
}
return syscall.Handle(r), nil
}
// attachStageShm 按名字打开 StageContext 段并映射。
func attachStageShm(size int) ([]byte, error) {
return openNamedMapping(os.Getenv(envStageShmName), size, "StageContext 段")
}
// attachEvtRingShm 按名字打开事件环段并映射。
func attachEvtRingShm(size int) ([]byte, error) {
return openNamedMapping(os.Getenv(envEvtRingName), size, "事件环段")
}
// openNamedMapping 打开命名共享段并映射为 []byte。
//
// 与 Unix 的 mmap 语义对齐MapViewOfFile 返回的地址在本进程虚拟空间,
// 段内偏移仍是相对的,故跨进程解引用正确。
func openNamedMapping(name string, size int, what string) ([]byte, error) {
if name == "" {
return nil, fmt.Errorf("%s 名字未经环境变量传入", what)
}
namePtr, err := syscall.UTF16PtrFromString(name)
if err != nil {
return nil, fmt.Errorf("%s 名字非法: %w", what, err)
}
h, err := openFileMappingW(syscall.FILE_MAP_WRITE, false, namePtr)
if err != nil {
return nil, fmt.Errorf("打开 %s%s: %w", what, name, err)
}
addr, err := syscall.MapViewOfFile(h, syscall.FILE_MAP_WRITE, 0, 0, uintptr(size))
if err != nil {
syscall.CloseHandle(h)
return nil, fmt.Errorf("映射 %s: %w", what, err)
}
// 句柄不关:视图存活期间必须保持句柄有效,进程退出时由 OS 回收。
return unsafe.Slice((*byte)(unsafe.Pointer(addr)), size), nil
}
// openEvtNotifier 按名字打开事件通知对象。
func openEvtNotifier() (evtWaiter, error) {
name := os.Getenv(envEvtEventName)
if name == "" {
return nil, fmt.Errorf("事件通知对象名字未经环境变量传入")
}
namePtr, err := syscall.UTF16PtrFromString(name)
if err != nil {
return nil, fmt.Errorf("事件对象名字非法: %w", err)
}
h, err := openEventW(winSynchronize|winEventModifyState, false, namePtr)
if err != nil {
return nil, fmt.Errorf("打开事件对象(%s: %w", name, err)
}
return &windowsEvtWaiter{h: h}, nil
}
// windowsEvtWaiter 用命名 Event 对象等待通知。
//
// 与 eventfd 的差异Event 是二元信号而非计数器,多次 SetEvent 只对应
// 一次唤醒。这不影响正确性——消费者被唤醒后按 readSeq 追 writeSeq
// 批量 drain一次唤醒能处理累积的全部事件。
//
// WaitForSingleObject 阻塞的是 OS 线程而非仅 goroutine故不如 eventfd
// 的 netpoller 路径省线程。每插件一个消费 goroutine17 插件即 17 线程,
// 在可接受范围(实验 5 实测 17 子进程共 84 线程)。
type windowsEvtWaiter struct {
h syscall.Handle
}
func (w *windowsEvtWaiter) Wait(buf []byte) error {
ev, err := syscall.WaitForSingleObject(w.h, syscall.INFINITE)
if err != nil {
return err
}
if ev != syscall.WAIT_OBJECT_0 {
return fmt.Errorf("等待事件对象返回 0x%x", ev)
}
return nil
}