refactor(plugin)!: 重编提示与模板路径改用 hmapdev;文档全面对齐 1.2.0

工具链在 SDK 1.2.0 更名为 hmapdev(原 plugindev)。核心侧三处功能耦合同步:

1. 用户可见报错:旧 C ABI 产物 / 协议版本不匹配 / 共享段版本不匹配
   三处「请用配套 plugindev 重编」→ hmapdev(对应两条测试断言同步)
2. e2e_template_test 的模板路径改为 tools/hmapdev/templates,
   并保留旧路径回退(旧 SDK 检出仍能跑测试)
3. 注释与文档同步

文档更新(用户可见面):
- assets/docs/{zh,en}/PLUGIN_DEV.md:工具链章节整体改为 hmapdev,
  补改名说明与 SDK 存储目录迁移;命令示例全部更新
- assets/docs/{zh,en}/ARCHITECTURE.md:**流程图与章节对齐 v1.2.0** ——
  · 向量化章节改为三层降级:统一多模态空间(主)→ 词嵌入 → TF-IDF(回退),
    写明「同指纹且同维度才参与融合」
  · 媒体记忆章节重写:媒体是一等记忆块(无独立 GC / 无引用计数 / 无描述式索引 /
    正文不再写 media marker),并写明 reembedStaleMedia 的跨空间迁移与写回
- README{,_EN}.md、docs/zh/plugin-interface-matrix.md:工具名与模板路径同步
  (历史条目标注「当时名为 plugindev」)

验证:go test ./internal/plugin/ ./internal/plugin/proc/ ok,
含 4 条真实模板 E2E(模板路径切换后仍通过)。
This commit is contained in:
JianFeeeee
2026-09-12 12:38:38 +08:00
parent 34628720f2
commit b15d0bd114
18 changed files with 187 additions and 146 deletions

View File

@ -190,7 +190,7 @@ internal/
├── config/ SQLite 配置中心 ├── config/ SQLite 配置中心
├── events/ 事件总线 ├── events/ 事件总线
└── internal/lua/adapters/ 8 个 LLM 协议适配器脚本 └── internal/lua/adapters/ 8 个 LLM 协议适配器脚本
外部插件开发见 [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 仓库,使用 `plugindev` 工具链开发,参考 `example/` 目录下的 Go 和 Lua 示例 外部插件开发见 [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 仓库,使用 `hmapdev` 工具链开发,参考 `example/` 目录下的 Go 和 Lua 示例
``` ```
## 项目状态 ## 项目状态
@ -209,7 +209,7 @@ internal/
是块的**迁移**,不是复制、也不靠引用保活。 是块的**迁移**,不是复制、也不靠引用保活。
- **数据面全部走共享内存**(工具调用帧 / Cleaner / 输入输出通道 / 媒体块 / 文档与知识正文), - **数据面全部走共享内存**(工具调用帧 / Cleaner / 输入输出通道 / 媒体块 / 文档与知识正文),
RPC 只传偏移描述符;**RPC 协议升到 2**fd3 布局改变,**不支持滚动升级**—— RPC 只传偏移描述符;**RPC 协议升到 2**fd3 布局改变,**不支持滚动升级**——
内核与全部插件必须同批重建、同批安装,存量插件须用新版 `plugindev` 重编。 内核与全部插件必须同批重建、同批安装,存量插件须用新版 `hmapdev` 重编。
- 注入可声明 `InjectOptions{NoMemory, ContextPolicy}`**默认仍记入记忆、默认不裁剪** - 注入可声明 `InjectOptions{NoMemory, ContextPolicy}`**默认仍记入记忆、默认不裁剪**
裁剪必须显式声明,且先经插件注册的 `Cleaner`。SDK 1.2.0 相对 1.1.0 **纯追加** 裁剪必须显式声明,且先经插件注册的 `Cleaner`。SDK 1.2.0 相对 1.1.0 **纯追加**
- **发行包默认启用** ONNX 向量空间并把模型754MB与 ONNX Runtime24MB - **发行包默认启用** ONNX 向量空间并把模型754MB与 ONNX Runtime24MB
@ -226,7 +226,7 @@ internal/
**v1.1.0** — 记忆系统支持**二进制多媒体节点**。内容寻址媒体存储CAS + SQLite 元数据 + 磁盘 blob`Get` always 重校 digest贯通 L0上下文事件/L2文档/L3图谱句子三层引用计数式 GC有引用者绝不删。视觉模型生成的描述文本是持久语义记忆blob 只是可被容量 GC 淘汰的缓存。 **v1.1.0** — 记忆系统支持**二进制多媒体节点**。内容寻址媒体存储CAS + SQLite 元数据 + 磁盘 blob`Get` always 重校 digest贯通 L0上下文事件/L2文档/L3图谱句子三层引用计数式 GC有引用者绝不删。视觉模型生成的描述文本是持久语义记忆blob 只是可被容量 GC 淘汰的缓存。
**v1.0.0** — 外部插件从 C ABI 动态库迁移到**子进程 + 共享内存**。首个不再加载 `.so`/`.dll` 的版本,与 0.9.x 不兼容(存量插件须用新版 `plugindev` 重编为 `plugin.bin`**业务代码零改动**)。消除 6 类此前在生产造成故障的缺陷:热重载失效(`DF_1_NODELETE``dlclose` 成 no-op、崩溃隔离缺失插件 panic 带崩 homed、stage lost update副本模型丢失 35.8~36.8%、cgo 超时不可中断(线程线性泄漏)、`output_send` 假成功模型收到「已发送」而消息未送达、Windows 能力断层(只见 3 个 stage 字段且无法写回。三面通信stdio JSON-RPC控制+ 共享内存段(数据)+ 事件环通知权限梯度显式化为三道闸。RPC 往返 p50 24.1µs崩溃到恢复 <1s **v1.0.0** — 外部插件从 C ABI 动态库迁移到**子进程 + 共享内存**。首个不再加载 `.so`/`.dll` 的版本,与 0.9.x 不兼容(存量插件须用新版工具链重编;该工具链当时名为 `plugindev`**现名 `hmapdev`**)。外部插件需重编为 `plugin.bin`**业务代码零改动**)。消除 6 类此前在生产造成故障的缺陷:热重载失效(`DF_1_NODELETE``dlclose` 成 no-op、崩溃隔离缺失插件 panic 带崩 homed、stage lost update副本模型丢失 35.8~36.8%、cgo 超时不可中断(线程线性泄漏)、`output_send` 假成功模型收到「已发送」而消息未送达、Windows 能力断层(只见 3 个 stage 字段且无法写回。三面通信stdio JSON-RPC控制+ 共享内存段(数据)+ 事件环通知权限梯度显式化为三道闸。RPC 往返 p50 24.1µs崩溃到恢复 <1s
**v0.9.0** C ABI v2外部插件 Stage 回调支持写回`invoke_stage` 增加 result 输出插件可在 OnInput/AfterToolcall/PostAction 修改 RawMessage/LLMText/ToolResults 等并同步回内核ABI 版本随内核 minor 对齐v0.9.x ABIVersion=2`version_min=1` 向后兼容旧插件)。同步修复工具循环 zen 兼容补位误伤首轮 system 上下文的问题配套 SDK 提供增强版 sanitizer 示例 UTF-8/U+FFFD/ANSI 转义全链路清洗)。** ABI 已随 v1.0.0 退场。** **v0.9.0** C ABI v2外部插件 Stage 回调支持写回`invoke_stage` 增加 result 输出插件可在 OnInput/AfterToolcall/PostAction 修改 RawMessage/LLMText/ToolResults 等并同步回内核ABI 版本随内核 minor 对齐v0.9.x ABIVersion=2`version_min=1` 向后兼容旧插件)。同步修复工具循环 zen 兼容补位误伤首轮 system 上下文的问题配套 SDK 提供增强版 sanitizer 示例 UTF-8/U+FFFD/ANSI 转义全链路清洗)。** ABI 已随 v1.0.0 退场。**

View File

@ -179,7 +179,7 @@ internal/
├── config/ SQLite config center ├── config/ SQLite config center
├── events/ Event bus ├── events/ Event bus
└── internal/lua/adapters/ 8 LLM protocol adapter scripts └── internal/lua/adapters/ 8 LLM protocol adapter scripts
External plugin development: see [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repo, use `plugindev` toolchain, refer to Go and Lua examples in `example/` External plugin development: see [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repo, use `hmapdev` toolchain, refer to Go and Lua examples in `example/`
``` ```
## Project Status ## Project Status
@ -240,7 +240,7 @@ by `-race`; in production this showed up as sporadic nil-dereference crashes dur
items are never deleted). The description text produced by the vision model is the durable items are never deleted). The description text produced by the vision model is the durable
semantic memory; the blob is only a cache that capacity GC may evict. semantic memory; the blob is only a cache that capacity GC may evict.
**v1.0.0** — External plugins moved from C ABI shared libraries to **subprocess + shared memory**. The first release that no longer loads `.so`/`.dll`, and it is incompatible with 0.9.x (existing plugins must be rebuilt into `plugin.bin` with the new `plugindev`, though **business code needs zero changes**). Eliminates 6 classes of defects that had caused production incidents: hot-reload silently failing (`DF_1_NODELETE` making `dlclose` a no-op), no crash isolation (a plugin panic took down homed), stage lost updates (35.8~36.8% loss under the copy model), uncancellable cgo timeouts (linear OS-thread leaks), `output_send` reporting false success (the model was told "sent" while the message never went out), and Windows capability degradation (only 3 stage fields visible, no write-back). Three communication planes: stdio JSON-RPC (control) + shared memory segment (data) + event ring (notification); the privilege gradient is now enforced by three explicit gates. RPC round-trip p50 24.1µs; crash-to-recovery under 1s. **v1.0.0** — External plugins moved from C ABI shared libraries to **subprocess + shared memory**. The first release that no longer loads `.so`/`.dll`, and it is incompatible with 0.9.x (existing plugins must be rebuilt into `plugin.bin` with the new toolchain — called `plugindev` back then, **now `hmapdev`** though **business code needs zero changes**). Eliminates 6 classes of defects that had caused production incidents: hot-reload silently failing (`DF_1_NODELETE` making `dlclose` a no-op), no crash isolation (a plugin panic took down homed), stage lost updates (35.8~36.8% loss under the copy model), uncancellable cgo timeouts (linear OS-thread leaks), `output_send` reporting false success (the model was told "sent" while the message never went out), and Windows capability degradation (only 3 stage fields visible, no write-back). Three communication planes: stdio JSON-RPC (control) + shared memory segment (data) + event ring (notification); the privilege gradient is now enforced by three explicit gates. RPC round-trip p50 24.1µs; crash-to-recovery under 1s.
**v0.9.0** — C ABI v2: external plugin Stage callbacks can now write back (`invoke_stage` gained a result out-param; plugins may mutate RawMessage/LLMText/ToolResults etc. in OnInput/AfterToolcall/PostAction and have them synced to the core). ABI version now tracks core minor releases (v0.9.x → ABIVersion=2, `version_min=1` keeps old plugins loadable). Also fixes the tool-loop zen-compat placeholder that wrongly fired on first-turn system context tail. The SDK ships an enhanced sanitizer example (bad-UTF-8 / U+FFFD / ANSI-escape scrub across the whole pipeline). **This ABI retired with v1.0.0.** **v0.9.0** — C ABI v2: external plugin Stage callbacks can now write back (`invoke_stage` gained a result out-param; plugins may mutate RawMessage/LLMText/ToolResults etc. in OnInput/AfterToolcall/PostAction and have them synced to the core). ABI version now tracks core minor releases (v0.9.x → ABIVersion=2, `version_min=1` keeps old plugins loadable). Also fixes the tool-loop zen-compat placeholder that wrongly fired on first-turn system context tail. The SDK ships an enhanced sanitizer example (bad-UTF-8 / U+FFFD / ANSI-escape scrub across the whole pipeline). **This ABI retired with v1.0.0.**

View File

@ -132,11 +132,22 @@ Setting `ctx.Response` at any stage jumps to `after_output`.
→ triples → GraphDB.Commit → triples → GraphDB.Commit
``` ```
### Vectorization: Pretrained Word Embedding + TF-IDF Fallback ### Vectorization: Unified Multimodal Space (primary) → Word Embedding TF-IDF (fallback)
All vectorization unified under `StaticEmbedder` (`internal/memory/static_embedder.go`): Vectorization degrades through three layers by availability; **each missing layer reports an explicit
error and never pretends to succeed**:
**Primary Strategy — Pretrained Word Embedding (aligned 300d)** **① Unified multimodal space (primary path, since v1.2.0)**
Text and images share **one model, one dimension, one fingerprint** (default `chineseclip`: 512d,
Apache-2.0, Chinese-native; `qwen3vl` or an external `http` provider are alternatives).
Providers register through the public `pkg/embedding` SPI — **the kernel hardcodes no model**.
Vectors persist together with their fingerprint (`dense_fp` / `vec_model`); any mismatch with the
current fingerprint triggers recomputation, and only blocks with the **same fingerprint and the same
dimension** participate in fusion (mixing coordinate systems yields a direction resembling neither).
**② Word embedding (text fallback)** — `StaticEmbedder` (`internal/memory/static_embedder.go`):
**Model sources** (aligned 300d)
- Model sources: ConceptNet Numberbatch (77-language aligned) / fastText Chinese / fastText English - Model sources: ConceptNet Numberbatch (77-language aligned) / fastText Chinese / fastText English
- Configured via `core.agent.embedding_model_path` (comma-separated multi-model) - Configured via `core.agent.embedding_model_path` (comma-separated multi-model)
- Path containing `numberbatch` → auto-download ConceptNet; `cc.zh.` → fastText Chinese; `cc.en.` → fastText English - Path containing `numberbatch` → auto-download ConceptNet; `cc.zh.` → fastText Chinese; `cc.en.` → fastText English
@ -196,28 +207,32 @@ single typo silently breaks reference binding with no error anywhere in the chai
also gained `sentence_text`: media references hang off a sentence, so with no sentence there is also gained `sentence_text`: media references hang off a sentence, so with no sentence there is
nowhere to attach them. nowhere to attach them.
### Media Memory (since v1.1.0) ### Media Memory (since v1.2.0: first-class memory blocks)
`internal/memory/media/``Store`, content-addressed (CAS) Media is not attached content but a **first-class memory node**: `internal/memory/media/` is a
content-addressed store (CAS), and graph `block` nodes carry its digest plus its own vector, while
structural edges (e.g. `sentence --contains--> block`) express ownership.
| Concern | Approach | Why | | Concern | Approach | Why |
|---|---|---| |---|---|---|
| Addressing | sha256 digest; metadata in SQLite, blobs on disk | Identical bytes stored once; metadata must be queryable, blobs must not live in the database | | Addressing | sha256 digest; metadata in SQLite, blobs on disk (`blobs/<first2>/<rest>`, two-level fanout) | Identical bytes stored once; metadata must be queryable, blobs must not live in the database |
| Integrity | Every `Get` re-verifies the digest | Silently returning corrupt data on disk damage is far worse than an error | | Integrity | Every `Get` re-verifies the digest | Silently returning corrupt data on disk damage is far worse than an error |
| Write atomicity | `.tmp` + rename | A half-written file taken as complete content would permanently poison that digest | | Write atomicity | `.tmp` + rename | A half-written file taken as complete content would permanently poison that digest |
| References | `owner_kind/owner_id/digest` composite primary key, `AddRef` idempotent | Three owner kinds: `context` (context events), `document`, `graph_sentence` | | Retrieval | Blocks carry **their own multimodal vector and fingerprint** and are searched directly | No description text is needed as an intermediary |
| GC | Two-stage with `minAge`, **referenced items are never deleted** | Description text stays in the text layers while blobs may be evicted — semantic memory and byte cache are decoupled | | Lifecycle | **No separate GC, no refcounts, no keep-set**; deleting the block deletes the content | Media is a memory node, not a cache that needs keeping alive |
**How media is represented in plain-text memory** is the marker `[<mime> <short digest>] <description>`: **Description-based indexing is gone**: the old implementation embedded a
`[<mime> <short digest>] <description>` marker in the body and treated the description as the
semantic memory (retrieval used it). That path was removed wholesale in v1.2.0: a description is
second-hand model output, and retrieving "someone else's paraphrase of an image" is strictly worse
than retrieving the image's own vector. Images are now retrieved only by their own vector in the
unified space, and no media marker is written into the body.
``` **Cross-space vector migration**: media rows store their vector together with `vec_model` (the space
[image/png a1b2c3d4e5f6] a purple-blue-red three-band chart fingerprint). At startup `reembedStaleMedia()` recomputes and **writes back** every row whose
``` `vec_model` is empty (never embedded) or differs from the current space (model/dimension switched).
Modalities outside the space return `ErrModalityUnsupported` — the kernel **never substitutes
Why it must ride on text: `Doc.Content`, `sentences.text` and text memory's `Input` are all strings another model's vector**.
— there is no field to carry structured data. **The description text is the durable semantic
memory** (retrieval uses it); the digest is the key back to the bytes (reverse lookup uses it).
After capacity GC evicts a blob, the description remains in the L0/L2/L3 text.
The media store is **optional throughout**: with `core.memory.media.enabled=false` or no The media store is **optional throughout**: with `core.memory.media.enabled=false` or no
configuration, the whole chain silently degrades to plain-text behaviour — no errors, no panics. configuration, the whole chain silently degrades to plain-text behaviour — no errors, no panics.
@ -292,7 +307,7 @@ VM built-ins: `json.encode` / `json.decode` / `log` / `http_get` / `http_post`.
| Method | Registration Mechanism | Compilation | Usage | | Method | Registration Mechanism | Compilation | Usage |
|--------|----------------------|-------------|-------| |--------|----------------------|-------------|-------|
| Built-in | `init()``RegisterFactory` | `internal/plugins/` compiled into kernel | webui/cli/timer/mcp etc. | | Built-in | `init()``RegisterFactory` | `internal/plugins/` compiled into kernel | webui/cli/timer/mcp etc. |
| External subprocess plugin | Handshake + stdio JSON-RPC reverse registration | `plugindev build``plugin.bin` (ordinary Go binary) | qq/browser/files etc. | | External subprocess plugin | Handshake + stdio JSON-RPC reverse registration | `hmapdev build``plugin.bin` (ordinary Go binary) | qq/browser/files etc. |
| Lua script plugin | Execute `main.lua` to register tools | No compilation, takes effect after restart/reload | luademo etc. | | Lua script plugin | Execute `main.lua` to register tools | No compilation, takes effect after restart/reload | luademo etc. |
| SKILL plugin | Parse `SKILL.md` | Markdown definition | Loaded via clawhubadapter | | SKILL plugin | Parse `SKILL.md` | Markdown definition | Loaded via clawhubadapter |
@ -311,7 +326,7 @@ Lua script plugin loading: `internal/plugin/` → the gopher-lua interpreter exe
| Dimension | Built-in Plugin | External Plugin | | Dimension | Built-in Plugin | External Plugin |
|-----------|----------------|-----------------| |-----------|----------------|-----------------|
| Registration | `init()` calls `plugin.RegisterFactory(name, factory)` | Implements `NewPluginFactory(name, config) (sdk.Plugin, error)` entry function | | Registration | `init()` calls `plugin.RegisterFactory(name, factory)` | Implements `NewPluginFactory(name, config) (sdk.Plugin, error)` entry function |
| Compilation | Compiled into `homed` binary, no separate build | Compiled via `plugindev build` to `plugin.bin` (ordinary Go binary, zero cgo); the kernel spawns it as a subprocess | | Compilation | Compiled into `homed` binary, no separate build | Compiled via `hmapdev build` to `plugin.bin` (ordinary Go binary, zero cgo); the kernel spawns it as a subprocess |
| Distribution | Bundled with kernel, not independently installable | `.hmap` package (ZIP archive), installed via WebUI or pluginmgr API | | Distribution | Bundled with kernel, not independently installable | `.hmap` package (ZIP archive), installed via WebUI or pluginmgr API |
| Metadata | `plugin.RegisterPluginMeta()` for display name | `plugin.json` manifest file (name, version, entry, platforms, capabilities, etc.) | | Metadata | `plugin.RegisterPluginMeta()` for display name | `plugin.json` manifest file (name, version, entry, platforms, capabilities, etc.) |
| Plugin directory | No separate directory, compiled into binary | `plugins/<name>/` independent directory with `plugin.json` + `plugin.bin` | | Plugin directory | No separate directory, compiled into binary | `plugins/<name>/` independent directory with `plugin.json` + `plugin.bin` |

View File

@ -29,75 +29,80 @@ type Plugin interface {
| Method | Use Case | Complexity | | Method | Use Case | Complexity |
|--------|----------|------------| |--------|----------|------------|
| **Subprocess plugin (recommended)** | Independently distributed third-party plugins | Medium, generated using `plugindev` toolchain | | **Subprocess plugin (recommended)** | Independently distributed third-party plugins | Medium, generated using `hmapdev` toolchain |
| **Built-in plugin** | Released with HomeAgent | Simple, requires merging into main repo | | **Built-in plugin** | Released with HomeAgent | Simple, requires merging into main repo |
| **Lua script plugin** | Lightweight rapid prototyping | Simple, generated using `plugindev init --lua` | | **Lua script plugin** | Lightweight rapid prototyping | Simple, generated using `hmapdev init --lua` |
--- ---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> : <img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 1. Quick Start: Using the plugindev Toolchain ## 1. Quick Start: Using the hmapdev Toolchain
`plugindev` is the unified plugin development toolchain provided in the SDK repository, supporting both Go and Lua plugin types. `hmapdev` is the unified plugin development toolchain provided in the SDK repository, supporting both Go and Lua
plugin types, and producing `.hmap` plugin bundles (the tool is named after that package format).
> Rename note: as of 1.2.0 the toolchain was renamed from `plugindev` to `hmapdev`; the SDK store moved from
> `~/.homeagent/plugindev/sdk` to `~/.homeagent/hmapdev/sdk` (the old directory keeps working automatically).
### Installation ### Installation
```bash ```bash
cd homeagent-sdk/tools/plugindev cd homeagent-sdk/tools/hmapdev
go build -o plugindev go build -o hmapdev
# Add plugindev to PATH or use directly # Add hmapdev to PATH or use directly
# Prebuilt binaries also ship as release assets (hmapdev_linux_amd64, ...)
``` ```
### SDK Version Management ### SDK Version Management
`plugindev sdk` manages local SDK versions: `hmapdev sdk` manages local SDK versions:
```bash ```bash
plugindev sdk list # list installed SDK versions hmapdev sdk list # list installed SDK versions
plugindev sdk current # show current SDK version hmapdev sdk current # show current SDK version
plugindev sdk latest # show latest available version hmapdev sdk latest # show latest available version
plugindev sdk install v0.8.0 # install a specific version hmapdev sdk install v1.2.0 # install a specific version
plugindev sdk use v0.8.0 # switch to a version hmapdev sdk use v1.2.0 # switch to a version
plugindev sdk path # show current SDK path hmapdev sdk path # show current SDK path
``` ```
SDK is stored at `~/.homeagent/plugindev/sdk/<version>/`; `plugindev init` reads the current SDK version for `go.mod`. SDK is stored at `~/.homeagent/hmapdev/sdk/<version>/`; `hmapdev init` reads the current SDK version for `go.mod`.
### Source Debugging ### Source Debugging
`plugindev debug` interprets plugin source and prints a call trace, no compilation environment needed: `hmapdev debug` interprets plugin source and prints a call trace, no compilation environment needed:
```bash ```bash
plugindev debug [dir] # dir defaults to the current directory hmapdev debug [dir] # dir defaults to the current directory
``` ```
### Creating a Go Plugin ### Creating a Go Plugin
```bash ```bash
plugindev init myplugin hmapdev init myplugin
cd myplugin cd myplugin
# Edit plugin code # Edit plugin code
vim plugin.go vim plugin.go
# Build and package (default is a multi-platform bundle, see below) # Build and package (default is a multi-platform bundle, see below)
plugindev build hmapdev build
# Output: dist/myplugin_bundle.hmap # Output: dist/myplugin_bundle.hmap
# Single-platform build: # Single-platform build:
plugindev build --no-bundle hmapdev build --no-bundle
# Output: dist/myplugin_linux_amd64.hmap (or windows_amd64) # Output: dist/myplugin_linux_amd64.hmap (or windows_amd64)
``` ```
### Creating a Lua Plugin ### Creating a Lua Plugin
```bash ```bash
plugindev init myluaplugin --lua hmapdev init myluaplugin --lua
cd myluaplugin cd myluaplugin
# Edit plugin code # Edit plugin code
vim main.lua vim main.lua
# Local test # Local test
lua main.lua lua main.lua
# Build and package # Build and package
plugindev build hmapdev build
# Output: dist/myluaplugin_lua.hmap # Output: dist/myluaplugin_lua.hmap
``` ```
@ -128,16 +133,16 @@ myluaplugin/
### Build & Package ### Build & Package
`plugindev build` automatically handles compilation and packaging: `hmapdev build` automatically handles compilation and packaging:
```bash ```bash
cd myplugin cd myplugin
plugindev build # default bundle mode (multi-platform) hmapdev build # default bundle mode (multi-platform)
plugindev build --no-bundle # single-target build (per plg.json targets) hmapdev build --no-bundle # single-target build (per plg.json targets)
plugindev build --target linux/amd64 # append a target on top of plg.json targets hmapdev build --target linux/amd64 # append a target on top of plg.json targets
plugindev build --outdir dist # output directory (default: dist) hmapdev build --outdir dist # output directory (default: dist)
plugindev build --sdk-path <path> # SDK path override (go.mod replace) hmapdev build --sdk-path <path> # SDK path override (go.mod replace)
plugindev build --replace <mod@path> # append a go.mod replace directive (repeatable) hmapdev build --replace <mod@path> # append a go.mod replace directive (repeatable)
``` ```
Execution process: Execution process:
@ -171,7 +176,7 @@ the kernel picks the one matching the current platform and renames it to `plugin
> - `plugin.so` / `plugin.dylib` / `plugin.dll` are **no longer loaded**. The new kernel > - `plugin.so` / `plugin.dylib` / `plugin.dll` are **no longer loaded**. The new kernel
> skips legacy artifacts with an actionable error instead of crashing. > skips legacy artifacts with an actionable error instead of crashing.
> - **Business code needs no changes** — the public SDK interface is unchanged; just > - **Business code needs no changes** — the public SDK interface is unchanged; just
> rebuild with the new `plugindev`. > rebuild with the new `hmapdev` (formerly `plugindev`).
> - The `entry` field in `plg.json` is **meaningless for Go plugins** now (leaving > - The `entry` field in `plg.json` is **meaningless for Go plugins** now (leaving
> `plugin.so` there is harmless); it only distinguishes Lua plugins. > `plugin.so` there is harmless); it only distinguishes Lua plugins.
> - Artifacts no longer need cgo, so cross-compiling requires no target C toolchain. > - Artifacts no longer need cgo, so cross-compiling requires no target C toolchain.
@ -180,12 +185,12 @@ the kernel picks the one matching the current platform and renames it to `plugin
### Build Targets & Multi-platform Bundle ### Build Targets & Multi-platform Bundle
**`plugindev build` defaults to bundle mode** (unless `plg.json` explicitly sets `"bundle": false`): it builds linux/amd64 + darwin/amd64 + windows/amd64 in one pass, producing a single `.hmap` with all platform binaries. The output manifest includes a `platforms` field. The kernel auto-selects the correct binary during installation. **`hmapdev build` defaults to bundle mode** (unless `plg.json` explicitly sets `"bundle": false`): it builds linux/amd64 + darwin/amd64 + windows/amd64 in one pass, producing a single `.hmap` with all platform binaries. The output manifest includes a `platforms` field. The kernel auto-selects the correct binary during installation.
```bash ```bash
plugindev build # default bundle, outputs dist/myplugin_bundle.hmap hmapdev build # default bundle, outputs dist/myplugin_bundle.hmap
plugindev build --bundle # explicitly enable bundle (same as above) hmapdev build --bundle # explicitly enable bundle (same as above)
plugindev build --no-bundle # disable bundle, build per plg.json targets hmapdev build --no-bundle # disable bundle, build per plg.json targets
``` ```
Notes: Notes:
@ -275,7 +280,7 @@ func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, e
### Entry Point ### Entry Point
`plugindev init` generates `plugin.go` with the `NewPlugin` export function directly, `hmapdev init` generates `plugin.go` with the `NewPlugin` export function directly,
which is the entry point when the kernel loads the plugin: which is the entry point when the kernel loads the plugin:
```go ```go
@ -284,7 +289,7 @@ func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
} }
``` ```
At build time, `plugindev build` auto-generates subprocess runtime code At build time, `hmapdev build` auto-generates subprocess runtime code
(`z_proc_gen.go` for the platform-independent part, plus `z_proc_shm_unix.go` / (`z_proc_gen.go` for the platform-independent part, plus `z_proc_shm_unix.go` /
`z_proc_shm_windows.go`). All three platforms share the same entry point and the same `z_proc_shm_windows.go`). All three platforms share the same entry point and the same
RPC logic; only the cross-process resource-passing mechanism differs (inherited fds on RPC logic; only the cross-process resource-passing mechanism differs (inherited fds on

View File

@ -132,11 +132,20 @@ eventLoop() → processTextInput()
→ 三元组 → GraphDB.Commit → 三元组 → GraphDB.Commit
``` ```
### 向量化:预训练词嵌入 + TF-IDF 回退 ### 向量化:统一多模态空间(主)→ 词嵌入 TF-IDF回退
所有向量化统一使用 `StaticEmbedder``internal/memory/static_embedder.go` 向量化按可用性分三层降级,**每一层缺位都明确报错,不静默假装成功**
**主策略 — 预训练词嵌入(词对齐 300 ** **① 统一多模态空间主路径v1.2.0 **
文本与图像共用**同一模型、同一维度、同一指纹**(默认 `chineseclip`512 维、Apache-2.0、中文原生;
亦可选 `qwen3vl` 或外部 `http` provider。provider 经 `pkg/embedding` 公共 SPI 注册,
**内核不硬编码任何模型**。向量与指纹一起持久化(`dense_fp` / `vec_model`
与当前指纹不一致即触发重算;融合时只接受**同指纹且同维度**的块向量
(跨坐标系的向量混进去会算出两边都不像的方向)。
**② 词嵌入(文本兜底)** — `StaticEmbedder``internal/memory/static_embedder.go`
**模型来源**(词对齐 300 维)
- 模型来源ConceptNet Numberbatch77 语对齐)/ fastText 中文 / fastText 英文 - 模型来源ConceptNet Numberbatch77 语对齐)/ fastText 中文 / fastText 英文
- 通过 `core.agent.embedding_model_path` 配置(逗号分隔多模型) - 通过 `core.agent.embedding_model_path` 配置(逗号分隔多模型)
- 路径名含 `numberbatch` → 自动下载 ConceptNet`cc.zh.` → fastText 中文,含 `cc.en.` → fastText 英文 - 路径名含 `numberbatch` → 自动下载 ConceptNet`cc.zh.` → fastText 中文,含 `cc.en.` → fastText 英文
@ -194,30 +203,31 @@ eventLoop() → processTextInput()
要求调用方知道格式,等于让一个拼写错误静默切断引用绑定而全链路无人报错。 要求调用方知道格式,等于让一个拼写错误静默切断引用绑定而全链路无人报错。
`memory_commit` 同时新增 `sentence_text`:媒体引用挂在句子上,没有句子就无处可挂。 `memory_commit` 同时新增 `sentence_text`:媒体引用挂在句子上,没有句子就无处可挂。
### 媒体记忆v1.1.0 起) ### 媒体记忆v1.2.0 起:一等记忆块
`internal/memory/media/` `Store`内容寻址CAS 媒体不是外挂内容,而是**记忆的一等节点**`internal/memory/media/` 内容寻址仓储CAS
图数据库里的 block 节点携带它的 digest 与向量,结构边(如 `sentence --contains--> block`)表达归属。
| 关注点 | 做法 | 为何 | | 关注点 | 做法 | 为何 |
|---|---|---| |---|---|---|
| 寻址 | sha256 digest元数据在 SQLiteblob 在磁盘 | 相同字节只存一份元数据要可查询blob 不该进数据库 | | 寻址 | sha256 digest元数据在 SQLiteblob 在磁盘`blobs/<前2位>/<其余>` 两级分桶) | 相同字节只存一份元数据要可查询blob 不该进数据库 |
| 完整性 | 每次 `Get` 重校 digest | 磁盘损坏时静默返回脏数据比报错危险得多 | | 完整性 | 每次 `Get` 重校 digest | 磁盘损坏时静默返回脏数据比报错危险得多 |
| 写入原子性 | `.tmp` + rename | 半个文件被当成完整内容会永久污染那个 digest | | 写入原子性 | `.tmp` + rename | 半个文件被当成完整内容会永久污染那个 digest |
| 引用 | `owner_kind/owner_id/digest` 三元组主键,`AddRef` 幂等 | 三个 owner 类型:`context`(上下文事件)、`document`(文档)、`graph_sentence`(图谱句子) | | 检索 | 块携带**自己的多模态向量与指纹**,直接参与向量检索 | 不需要描述文本做中介 |
| GC | 两阶段 + `minAge`**有引用者绝不删** | 描述文本留在文本层blob 可淘汰——语义记忆与字节缓存分离 | | 生命周期 | **无独立 GC、无引用计数、无 keep-set**;删除块即删内容 | 媒体是记忆节点,不是需要保活的缓存 |
**媒体在纯文本记忆里的表示**是标记 `[<mime> <短digest>] <描述>` **不再有描述式索引**:旧实现在正文里写 `[<mime> <短digest>] <描述>` 标记,并把描述文本当作语义记忆
(检索靠描述)。该机制已在 v1.2.0 整体拆除:描述是模型生成的二手信息,
检索“别人转述的图片”不如检索图片自己的向量。现在图片只按自己的统一空间向量被检索,
正文里不再有 media marker。
``` **跨空间向量迁移**:媒体行的向量带 `vec_model`(空间指纹)。启动时
[image/png a1b2c3d4e5f6] 一张紫蓝红三色带图 `reembedStaleMedia()``vec_model` 为空(从未嵌入)或与当前空间不一致(换过模型/维度)的行
``` 批量重算并**写回库**;模态不在本空间覆盖范围时返回 `ErrModalityUnsupported`
**绝不拿别的模型的向量顶替**
之所以必须借文本承载:`Doc.Content``sentences.text`、文本记忆的 `Input` 全是字符串 媒体存储全程可选:`core.memory.media.enabled=false` 或未配置时,整条链路静默退化为纯文本行为
没有字段能挂结构化数据。**描述文本才是持久的语义记忆**检索靠它digest 是回到字节的 不报错不 panic。
钥匙反查靠它。blob 被容量 GC 淘汰后,描述仍留在 L0/L2/L3 的文本里。
媒体存储**全程可选**`core.memory.media.enabled=false` 或未配置时,整条链路静默退化为
纯文本行为,不报错不 panic。
### 其他记忆层 ### 其他记忆层
@ -287,7 +297,7 @@ VM 内置 `json.encode` / `json.decode` / `log` / `http_get` / `http_post`。
| 方式 | 注册机制 | 编译 | 用途 | | 方式 | 注册机制 | 编译 | 用途 |
|------|----------|------|------| |------|----------|------|------|
| 内置插件 | `init()``RegisterFactory` | `internal/plugins/` 编译进内核 | webui/cli/timer/mcp 等 | | 内置插件 | `init()``RegisterFactory` | `internal/plugins/` 编译进内核 | webui/cli/timer/mcp 等 |
| 外部子进程插件 | 握手 + stdio JSON-RPC 反向注册 | `plugindev build``plugin.bin`(普通 Go 二进制) | qq/browser/files 等 | | 外部子进程插件 | 握手 + stdio JSON-RPC 反向注册 | `hmapdev build``plugin.bin`(普通 Go 二进制) | qq/browser/files 等 |
| Lua 脚本插件 | 执行 `main.lua` 注册工具 | 无需编译,重启/重载生效 | luademo 等 | | Lua 脚本插件 | 执行 `main.lua` 注册工具 | 无需编译,重启/重载生效 | luademo 等 |
| SKILL 插件 | 解析 `SKILL.md` | Markdown 定义 | clawhubadapter 兼容加载 | | SKILL 插件 | 解析 `SKILL.md` | Markdown 定义 | clawhubadapter 兼容加载 |
@ -306,7 +316,7 @@ Lua 脚本插件加载:`internal/plugin/` → gopher-lua 解释器执行 `main
| 维度 | 内置插件 | 外部插件 | | 维度 | 内置插件 | 外部插件 |
|------|----------|----------| |------|----------|----------|
| 注册方式 | `init()` 调用 `plugin.RegisterFactory(name, factory)` | 实现 `NewPluginFactory(name, config) (sdk.Plugin, error)` 入口函数 | | 注册方式 | `init()` 调用 `plugin.RegisterFactory(name, factory)` | 实现 `NewPluginFactory(name, config) (sdk.Plugin, error)` 入口函数 |
| 编译方式 | 编译进 `homed` 二进制,无需独立编译 | 通过 `plugindev build` 编译为 `plugin.bin`(普通 Go 二进制,零 cgo内核 spawn 为子进程 | | 编译方式 | 编译进 `homed` 二进制,无需独立编译 | 通过 `hmapdev build` 编译为 `plugin.bin`(普通 Go 二进制,零 cgo内核 spawn 为子进程 |
| 分发方式 | 随内核分发,不可独立安装/卸载 | `.hmap`ZIP 归档),通过 WebUI 或 pluginmgr API 安装 | | 分发方式 | 随内核分发,不可独立安装/卸载 | `.hmap`ZIP 归档),通过 WebUI 或 pluginmgr API 安装 |
| 元数据 | 通过 `plugin.RegisterPluginMeta()` 注册显示名 | `plugin.json` manifest 文件name, version, entry, platforms, capabilities 等) | | 元数据 | 通过 `plugin.RegisterPluginMeta()` 注册显示名 | `plugin.json` manifest 文件name, version, entry, platforms, capabilities 等) |
| 插件目录 | 无独立目录,编译进二进制 | `plugins/<name>/` 独立目录,包含 `plugin.json` + `plugin.bin` | | 插件目录 | 无独立目录,编译进二进制 | `plugins/<name>/` 独立目录,包含 `plugin.json` + `plugin.bin` |

View File

@ -30,75 +30,80 @@ type Plugin interface {
| 方式 | 适用场景 | 复杂度 | | 方式 | 适用场景 | 复杂度 |
|------|---------|--------| |------|---------|--------|
| **子进程插件(推荐)** | 独立分发的第三方插件 | 中等,使用 `plugindev` 工具链生成 | | **子进程插件(推荐)** | 独立分发的第三方插件 | 中等,使用 `hmapdev` 工具链生成 |
| **内置插件** | 随 HomeAgent 一起发布 | 简单,需合入主仓库 | | **内置插件** | 随 HomeAgent 一起发布 | 简单,需合入主仓库 |
| **Lua 脚本插件** | 轻量快速原型 | 简单,使用 `plugindev init --lua` 生成 | | **Lua 脚本插件** | 轻量快速原型 | 简单,使用 `hmapdev init --lua` 生成 |
--- ---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> : <img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 一、快速开始:使用 plugindev 工具链 ## 一、快速开始:使用 hmapdev 工具链
`plugindev` 是 SDK 仓库提供的统一插件开发工具链,支持 Go 和 Lua 两种插件类型 `hmapdev` 是 SDK 仓库提供的统一插件开发工具链,支持 Go 和 Lua 两种插件类型
最终产出 `.hmap` 插件包(工具名即来自这个包格式)。
> 改名说明1.2.0 起工具链由 `plugindev` 更名为 `hmapdev`SDK 存储目录同时由
> `~/.homeagent/plugindev/sdk` 迁到 `~/.homeagent/hmapdev/sdk`(旧目录会自动继续沿用)。
### 安装 ### 安装
```bash ```bash
cd homeagent-sdk/tools/plugindev cd homeagent-sdk/tools/hmapdev
go build -o plugindev go build -o hmapdev
# 将 plugindev 加入 PATH 或直接使用 # 将 hmapdev 加入 PATH 或直接使用
# 也可从 SDK 的 release 附件下载预编译二进制hmapdev_linux_amd64 等)
``` ```
### SDK 版本管理 ### SDK 版本管理
`plugindev sdk` 子命令管理本地 SDK 版本: `hmapdev sdk` 子命令管理本地 SDK 版本:
```bash ```bash
plugindev sdk list # 列出已安装的 SDK 版本 hmapdev sdk list # 列出已安装的 SDK 版本
plugindev sdk current # 显示当前使用的 SDK 版本 hmapdev sdk current # 显示当前使用的 SDK 版本
plugindev sdk latest # 显示最新可用版本 hmapdev sdk latest # 显示最新可用版本
plugindev sdk install v0.8.0 # 安装指定版本 hmapdev sdk install v1.2.0 # 安装指定版本
plugindev sdk use v0.8.0 # 切换使用版本 hmapdev sdk use v1.2.0 # 切换使用版本
plugindev sdk path # 显示当前 SDK 路径 hmapdev sdk path # 显示当前 SDK 路径
``` ```
SDK 存储在 `~/.homeagent/plugindev/sdk/<version>/``plugindev init` 自动读取当前 SDK 版本填充 `go.mod` SDK 存储在 `~/.homeagent/hmapdev/sdk/<version>/``hmapdev init` 自动读取当前 SDK 版本填充 `go.mod`
### 源码调试 ### 源码调试
`plugindev debug` 直接用解释器执行插件源码并输出调用轨迹,无需编译环境: `hmapdev debug` 直接用解释器执行插件源码并输出调用轨迹,无需编译环境:
```bash ```bash
plugindev debug [dir] # dir 默认当前目录 hmapdev debug [dir] # dir 默认当前目录
``` ```
### 创建 Go 插件 ### 创建 Go 插件
```bash ```bash
plugindev init myplugin hmapdev init myplugin
cd myplugin cd myplugin
# 编辑插件代码 # 编辑插件代码
vim plugin.go vim plugin.go
# 编译打包 # 编译打包
plugindev build # 默认多平台 bundle见下节 hmapdev build # 默认多平台 bundle见下节
# 输出: dist/myplugin_bundle.hmap # 输出: dist/myplugin_bundle.hmap
# 单平台构建: # 单平台构建:
plugindev build --no-bundle hmapdev build --no-bundle
# 输出: dist/myplugin_linux_amd64.hmap (或 windows_amd64) # 输出: dist/myplugin_linux_amd64.hmap (或 windows_amd64)
``` ```
### 创建 Lua 插件 ### 创建 Lua 插件
```bash ```bash
plugindev init myluaplugin --lua hmapdev init myluaplugin --lua
cd myluaplugin cd myluaplugin
# 编辑插件代码 # 编辑插件代码
vim main.lua vim main.lua
# 本地测试 # 本地测试
lua main.lua lua main.lua
# 编译打包 # 编译打包
plugindev build hmapdev build
# 输出: dist/myluaplugin_lua.hmap # 输出: dist/myluaplugin_lua.hmap
``` ```
@ -129,16 +134,16 @@ myluaplugin/
### 编译打包 ### 编译打包
`plugindev build` 会自动完成编译和打包: `hmapdev build` 会自动完成编译和打包:
```bash ```bash
cd myplugin cd myplugin
plugindev build # 默认 bundle 模式(多平台合集) hmapdev build # 默认 bundle 模式(多平台合集)
plugindev build --no-bundle # 单平台构建(仅当前 plg.json targets hmapdev build --no-bundle # 单平台构建(仅当前 plg.json targets
plugindev build --target linux/amd64 # 在 targets 基础上追加一个目标 hmapdev build --target linux/amd64 # 在 targets 基础上追加一个目标
plugindev build --outdir dist # 指定输出目录(默认 dist hmapdev build --outdir dist # 指定输出目录(默认 dist
plugindev build --sdk-path <path> # 指定 SDK 路径(覆盖 go.mod replace hmapdev build --sdk-path <path> # 指定 SDK 路径(覆盖 go.mod replace
plugindev build --replace <mod@path> # 追加 go.mod replace 指令(可多次) hmapdev build --replace <mod@path> # 追加 go.mod replace 指令(可多次)
``` ```
执行过程: 执行过程:
@ -154,7 +159,7 @@ plugindev build --replace <mod@path> # 追加 go.mod replace 指令(可多次
| 文件 | 用途 | 关键字段 | | 文件 | 用途 | 关键字段 |
|------|------|---------| |------|------|---------|
| `plg.json` | 项目元信息,由开发者维护 | `targets` — 单平台构建目标(如 `"linux/amd64,windows/amd64"``bundle` — 多平台合集开关(默认 `true`| | `plg.json` | 项目元信息,由开发者维护 | `targets` — 单平台构建目标(如 `"linux/amd64,windows/amd64"``bundle` — 多平台合集开关(默认 `true`|
| `plugin.json` | 构建产物清单,`plugindev build` 自动生成 | `entry` — 入口文件名;`platforms` — 声明的支持平台 | | `plugin.json` | 构建产物清单,`hmapdev build` 自动生成 | `entry` — 入口文件名;`platforms` — 声明的支持平台 |
每个目标生成单独的 `.hmap`。子进程插件是普通可执行文件,**不分平台后缀** 每个目标生成单独的 `.hmap`。子进程插件是普通可执行文件,**不分平台后缀**
@ -169,7 +174,7 @@ bundle 包内按 `plugin.bin.<goos>.<goarch>` 区分各平台,安装时内核
> >
> - `plugin.so` / `plugin.dylib` / `plugin.dll` **不再被加载**。新内核遇到旧产物 > - `plugin.so` / `plugin.dylib` / `plugin.dll` **不再被加载**。新内核遇到旧产物
> 会跳过并报可操作错误,不崩溃。 > 会跳过并报可操作错误,不崩溃。
> - **业务代码不需要改一行**——公开 SDK 接口零改动,只需用新版 `plugindev` 重编。 > - **业务代码不需要改一行**——公开 SDK 接口零改动,只需用新版 `hmapdev`(原 `plugindev`重编。
> - `plg.json` 的 `entry` 字段对 Go 插件**已无意义**(写着 `plugin.so` 也无妨), > - `plg.json` 的 `entry` 字段对 Go 插件**已无意义**(写着 `plugin.so` 也无妨),
> 它现在只用于区分 Lua 插件。 > 它现在只用于区分 Lua 插件。
> - 产物不再需要 cgo交叉编译无需目标平台 C 工具链。 > - 产物不再需要 cgo交叉编译无需目标平台 C 工具链。
@ -178,12 +183,12 @@ bundle 包内按 `plugin.bin.<goos>.<goarch>` 区分各平台,安装时内核
### 构建目标与多平台打包bundle ### 构建目标与多平台打包bundle
**`plugindev build` 默认就是 bundle 模式**`plg.json` 未显式写 `"bundle": false` 时):一次编译 linux/amd64 + darwin/amd64 + windows/amd64生成包含所有平台二进制的单 `.hmap`,输出清单自动添加 `platforms` 字段。安装时核心自动选择当前平台的二进制,跳过其他平台。 **`hmapdev build` 默认就是 bundle 模式**`plg.json` 未显式写 `"bundle": false` 时):一次编译 linux/amd64 + darwin/amd64 + windows/amd64生成包含所有平台二进制的单 `.hmap`,输出清单自动添加 `platforms` 字段。安装时核心自动选择当前平台的二进制,跳过其他平台。
```bash ```bash
plugindev build # 默认 bundle输出 dist/myplugin_bundle.hmap hmapdev build # 默认 bundle输出 dist/myplugin_bundle.hmap
plugindev build --bundle # 显式开启 bundle同上 hmapdev build --bundle # 显式开启 bundle同上
plugindev build --no-bundle # 关闭 bundle按 plg.json 的 targets 逐平台构建 hmapdev build --no-bundle # 关闭 bundle按 plg.json 的 targets 逐平台构建
``` ```
注意: 注意:
@ -273,7 +278,7 @@ func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, e
### 入口点 ### 入口点
`plugindev init` 生成的 `plugin.go` 中直接包含 `NewPlugin` 导出函数,它是内核加载插件时的入口: `hmapdev init` 生成的 `plugin.go` 中直接包含 `NewPlugin` 导出函数,它是内核加载插件时的入口:
```go ```go
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
@ -281,7 +286,7 @@ func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
} }
``` ```
编译时 `plugindev build` 自动生成子进程运行时代码(`z_proc_gen.go` 平台无关 + `z_proc_shm_unix.go` / `z_proc_shm_windows.go` 平台特定),无需手动编写。三平台共享同一入口与同一套 RPC 逻辑仅跨进程资源传递机制不同Unix 继承 fdWindows 命名内核对象)。 编译时 `hmapdev build` 自动生成子进程运行时代码(`z_proc_gen.go` 平台无关 + `z_proc_shm_unix.go` / `z_proc_shm_windows.go` 平台特定),无需手动编写。三平台共享同一入口与同一套 RPC 逻辑仅跨进程资源传递机制不同Unix 继承 fdWindows 命名内核对象)。
### PluginSDK 核心 API ### PluginSDK 核心 API

View File

@ -24,7 +24,7 @@ import (
// 所以选择:**原生 Windows 不提供 homed**。Windows 用户跑 WSL2—— // 所以选择:**原生 Windows 不提供 homed**。Windows 用户跑 WSL2——
// WSL2 里就是普通 linux/amd64走与我们测试矩阵完全相同的那条路径。 // WSL2 里就是普通 linux/amd64走与我们测试矩阵完全相同的那条路径。
// //
// 注意范围:只有 homed 如此。plugindev 工具链仍可在 Windows 上运行 // 注意范围:只有 homed 如此。hmapdev 工具链仍可在 Windows 上运行
// (在 Windows 上开发、为 WSL 构建 linux 插件是合理工作流)。 // (在 Windows 上开发、为 WSL 构建 linux 插件是合理工作流)。
func requireSupportedPlatform() { func requireSupportedPlatform() {
fmt.Fprintln(os.Stderr, "homed 不支持 Windows 原生运行。") fmt.Fprintln(os.Stderr, "homed 不支持 Windows 原生运行。")

View File

@ -2,7 +2,7 @@
> 状态:**完成 v3**2026-09-06——v2 的迁移已上生产(内核 v1.0.0v3 记录 v1.1.1 的公开接口**扩展**。 > 状态:**完成 v3**2026-09-06——v2 的迁移已上生产(内核 v1.0.0v3 记录 v1.1.1 的公开接口**扩展**。
> 目的:钉死「暴露给外部插件的接口不变」这一约束的**合同面**——迁移前、迁移后外部插件看到/调用的 SDK 接口完全一致; > 目的:钉死「暴露给外部插件的接口不变」这一约束的**合同面**——迁移前、迁移后外部插件看到/调用的 SDK 接口完全一致;
> 所有改造落在**核心homed 侧)+ 工具链plugindev**,外部插件业务代码零改动,只需用新 plugindev 重编。 > 所有改造落在**核心homed 侧)+ 工具链(hmapdev当时名为 plugindev**,外部插件业务代码零改动,只需用新工具链重编。
> >
> **结果(已验证)**`git diff third_party/homeagent-sdk/sdk/` 全程为空17 个 `example/*/plugin.go` 逐字节未改 > **结果(已验证)**`git diff third_party/homeagent-sdk/sdk/` 全程为空17 个 `example/*/plugin.go` 逐字节未改
> `git status example/` 无输出);生产 17 插件全部经子进程通道运行。 > `git status example/` 无输出);生产 17 插件全部经子进程通道运行。
@ -11,7 +11,7 @@
> 它要保的是「换运行模型不动业务代码」。迁移完成后SDK 需要能随功能演进而扩展, > 它要保的是「换运行模型不动业务代码」。迁移完成后SDK 需要能随功能演进而扩展,
> 否则多模态这类能力永远到不了插件手上。解除的边界见 §九:**只增不减,签名不改**。 > 否则多模态这类能力永远到不了插件手上。解除的边界见 §九:**只增不减,签名不改**。
> >
> 维护规则:每次改动公开 SDK 接口面 `third_party/homeagent-sdk/sdk/` 或模板 `tools/plugindev/templates/` 后, > 维护规则:每次改动公开 SDK 接口面 `third_party/homeagent-sdk/sdk/` 或模板 `tools/hmapdev/templates/` 后,
> 必须同步更新本矩阵。 > 必须同步更新本矩阵。
> >
> 权威编号plan.md 第 11 节11.1~11.9)。本文档只做接口面盘点,不做实现。 > 权威编号plan.md 第 11 节11.1~11.9)。本文档只做接口面盘点,不做实现。
@ -21,9 +21,9 @@
## 一、迁移的形状(一句话) ## 一、迁移的形状(一句话)
``` ```
今天: 外部插件 = example/*/plugin.go纯 Go ──plugindev c-shared──> plugin.so 今天: 外部插件 = example/*/plugin.go纯 Go ──hmapdev c-shared──> plugin.so
homed ──dlopen──> plugin.soC ABI bridge51 个整数 method id homed ──dlopen──> plugin.soC ABI bridge51 个整数 method id
之后: 外部插件 = example/*/plugin.go纯 Go一行不改 ──plugindev go build──> plugin.bin 之后: 外部插件 = example/*/plugin.go纯 Go一行不改 ──hmapdev go build──> plugin.bin
homed ──spawn──> plugin.binstdio JSON-RPC + shm + eventfd homed ──spawn──> plugin.binstdio JSON-RPC + shm + eventfd
``` ```
@ -33,8 +33,8 @@
|---|---|---| |---|---|---|
| 公开 SDK `third_party/homeagent-sdk/sdk/*.go` | ❌ 纯 Go | **不动**(接口面 = 合同) | | 公开 SDK `third_party/homeagent-sdk/sdk/*.go` | ❌ 纯 Go | **不动**(接口面 = 合同) |
| 外部插件业务代码 `example/*/plugin.go` | ❌ 纯 Go只 import 公开 SDK | **不动**(只重编) | | 外部插件业务代码 `example/*/plugin.go` | ❌ 纯 Go只 import 公开 SDK | **不动**(只重编) |
| bridge 模板 `tools/plugindev/templates.go``tmplLinuxBridge`/`tmplBridge` | ✅ cgo | **删除/替换**为 `tmplProcMain` | | bridge 模板 `tools/hmapdev/templates.go``tmplLinuxBridge`/`tmplBridge` | ✅ cgo | **删除/替换**为 `tmplProcMain` |
| `plugindev` 构建命令 | c-shared | 改普通 `go build` | | `hmapdev` 构建命令 | c-shared | 改普通 `go build` |
| homed `internal/plugin/cabi/`1096 行) | cgo | 删(已归入 plan 迁移收尾 5.2 | | homed `internal/plugin/cabi/`1096 行) | cgo | 删(已归入 plan 迁移收尾 5.2 |
| homed `internal/plugin/registry.go` 加载分派 | — | 改:按 `entry` 分派 `.so`/`.bin` | | homed `internal/plugin/registry.go` 加载分派 | — | 改:按 `entry` 分派 `.so`/`.bin` |
@ -300,7 +300,7 @@ C 结构体不好传函数指针(那是运气,任何人给 dispatch 加个 c
## 七、接口冻结检查点(全部已通过) ## 七、接口冻结检查点(全部已通过)
1.**阶段 2子进程通道原型**`plugindev` 重编 weather → `plugin.bin` → 端到端跑通。 1.**阶段 2子进程通道原型**`hmapdev` 重编 weather → `plugin.bin` → 端到端跑通。
验收weather 业务代码逐字节未改(`git status example/` 无输出)。 验收weather 业务代码逐字节未改(`git status example/` 无输出)。
2.**阶段 3共享内存**:子进程并发改写 StageContext 丢失率 = 0% 2.**阶段 3共享内存**:子进程并发改写 StageContext 丢失率 = 0%
`TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate` `TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate`
@ -349,7 +349,7 @@ tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent]
### 3. 生成模板必须同步接线,否则是**全体外部插件编译失败** ### 3. 生成模板必须同步接线,否则是**全体外部插件编译失败**
公开接口加方法时,`tools/plugindev/templates/proc_main.go.tmpl` 里的 `procIO` / 公开接口加方法时,`tools/hmapdev/templates/proc_main.go.tmpl` 里的 `procIO` /
`procDocMemory` 若不实现新方法,就不满足接口——**每个外部插件都编不过**,是硬失败 `procDocMemory` 若不实现新方法,就不满足接口——**每个外部插件都编不过**,是硬失败
不是软降级。v1.1.1 这一层是被 `go test` 抓出来的(`internal/plugin/proc` 的两个 不是软降级。v1.1.1 这一层是被 `go test` 抓出来的(`internal/plugin/proc` 的两个
E2E 用例编译失败),不是靠人工检查发现的。 E2E 用例编译失败),不是靠人工检查发现的。
@ -366,7 +366,7 @@ E2E 用例编译失败),不是靠人工检查发现的。
|---|---|---| |---|---|---|
| 存量插件源码零改动 | `cd example/<n> && go vet ./...`17 个) | ✅ 17/17 通过 | | 存量插件源码零改动 | `cd example/<n> && go vet ./...`17 个) | ✅ 17/17 通过 |
| 旧产物仍能建链 | 用 SDK 0.9.2 编的 `plugin.bin``TestRealPlugin_*` | ✅ 4/4 通过(握手校验 `ProtocolVersion=1`,不是 SDK 版本) | | 旧产物仍能建链 | 用 SDK 0.9.2 编的 `plugin.bin``TestRealPlugin_*` | ✅ 4/4 通过(握手校验 `ProtocolVersion=1`,不是 SDK 版本) |
| 模板已接线 | `cd tools/plugindev && go test ./...` | ✅ `TestProcTemplate_CoversAllCoreMethods` 含新 method | | 模板已接线 | `cd tools/hmapdev && go test ./...` | ✅ `TestProcTemplate_CoversAllCoreMethods` 含新 method |
| 并发安全 | `go test ./sdk/ -race -count=5` | ✅ 零 DATA RACE13 例压测) | | 并发安全 | `go test ./sdk/ -race -count=5` | ✅ 零 DATA RACE13 例压测) |
### v1.2.x 的接口扩展2026-09-12 ### v1.2.x 的接口扩展2026-09-12
@ -426,5 +426,5 @@ data URL 本身已是 base64 文本,包进二进制传输省不了空间,还
- `internal/plugin/proc/protocol.go` — 合同面 B 的代码实现(`Method*` 常量,取代已删的 bridge 模板) - `internal/plugin/proc/protocol.go` — 合同面 B 的代码实现(`Method*` 常量,取代已删的 bridge 模板)
- `internal/plugin/proc/shm.go` — 合同面 C 的代码实现(共享段布局与 18 字段枚举) - `internal/plugin/proc/shm.go` — 合同面 C 的代码实现(共享段布局与 18 字段枚举)
- `internal/plugin/proc/capability.go` — 权限梯度capability 组 + `withheldCapabilities` - `internal/plugin/proc/capability.go` — 权限梯度capability 组 + `withheldCapabilities`
- `third_party/homeagent-sdk/tools/plugindev/templates/` — 子进程运行时模板(三文件) - `third_party/homeagent-sdk/tools/hmapdev/templates/` — 子进程运行时模板(三文件)
- `docs/zh/experiments/plugin-arch/` — 18 项可行性实验 + `19-migration-verify/` 迁移执行期工具 - `docs/zh/experiments/plugin-arch/` — 18 项可行性实验 + `19-migration-verify/` 迁移执行期工具

View File

@ -9,7 +9,7 @@ var (
// //
// 1.0.0:外部插件从 C ABI 动态库迁到子进程 + 共享内存。 // 1.0.0:外部插件从 C ABI 动态库迁到子进程 + 共享内存。
// 这是首个不再加载 `.so`/`.dll` 的版本,与 0.9.x 不兼容(存量插件必须 // 这是首个不再加载 `.so`/`.dll` 的版本,与 0.9.x 不兼容(存量插件必须
// 用新版 plugindev 重编),故跃到主版本号。 // 用新版 hmapdev 重编),故跃到主版本号。
// 1.1.0记忆系统支持二进制多媒体节点——CAS 媒体存储 + L0/L2/L3 贯通。 // 1.1.0记忆系统支持二进制多媒体节点——CAS 媒体存储 + L0/L2/L3 贯通。
// 1.1.1:多模态贯通**插件边界**。内核实现公开 SDK 1.1.0 新增的媒体接口 // 1.1.1:多模态贯通**插件边界**。内核实现公开 SDK 1.1.0 新增的媒体接口
// doc.insertWithMedia、io.injectMedia / injectMediaSync / // doc.insertWithMedia、io.injectMedia / injectMediaSync /

View File

@ -17,7 +17,7 @@ const (
// //
// 保留这张表只为**给出明确错误**:插件目录里躺着 plugin.so 而内核不再认它时, // 保留这张表只为**给出明确错误**:插件目录里躺着 plugin.so 而内核不再认它时,
// 静默跳过会让「目录在但插件没加载」看起来像配置问题,而实际原因是需要用 // 静默跳过会让「目录在但插件没加载」看起来像配置问题,而实际原因是需要用
// 新版 plugindev 重编。 // 新版 hmapdev 重编。
var legacyCABIEntries = []string{"plugin.so", "plugin.dll", "plugin.dylib"} var legacyCABIEntries = []string{"plugin.so", "plugin.dll", "plugin.dylib"}
// entryKind 描述插件入口归属的加载通道。 // entryKind 描述插件入口归属的加载通道。

View File

@ -115,7 +115,7 @@ func TestHasLegacyCABIEntry(t *testing.T) {
}) })
} }
// 旧 .so 插件必须报「用新 plugindev 重编」而非静默跳过。 // 旧 .so 插件必须报「用新 hmapdev 重编」而非静默跳过。
func TestTryDynamic_LegacyCABIGivesActionableError(t *testing.T) { func TestTryDynamic_LegacyCABIGivesActionableError(t *testing.T) {
r := NewRegistry() r := NewRegistry()
defer r.closeProcHost() defer r.closeProcHost()
@ -129,7 +129,7 @@ func TestTryDynamic_LegacyCABIGivesActionableError(t *testing.T) {
} }
// 错误消息须指向解决办法,且明确业务代码无需改 // 错误消息须指向解决办法,且明确业务代码无需改
msg := err.Error() msg := err.Error()
for _, want := range []string{"plugindev", "plugin.bin", "业务代码"} { for _, want := range []string{"hmapdev", "plugin.bin", "业务代码"} {
if !strings.Contains(msg, want) { if !strings.Contains(msg, want) {
t.Errorf("错误消息应含 %q实际: %v", want, err) t.Errorf("错误消息应含 %q实际: %v", want, err)
} }

View File

@ -10,11 +10,11 @@ import (
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
) )
// 端到端:用**真实 plugindev 模板**编译的插件,经内核 proc 通道加载运行。 // 端到端:用**真实 hmapdev 模板**编译的插件,经内核 proc 通道加载运行。
// //
// 与 plugin_test.go 中 testdata/*.go 假插件的区别: // 与 plugin_test.go 中 testdata/*.go 假插件的区别:
// 那些是手写的最简 RPC 实现,只验证内核侧逻辑; // 那些是手写的最简 RPC 实现,只验证内核侧逻辑;
// 这里用的是 tools/plugindev/templates/proc_main.go.tmpl —— 外部插件作者 // 这里用的是 tools/hmapdev/templates/proc_main.go.tmpl —— 外部插件作者
// 真正会拿到的那份运行时。它验证的是「模板 ↔ 内核」两侧协议/布局真的对齐, // 真正会拿到的那份运行时。它验证的是「模板 ↔ 内核」两侧协议/布局真的对齐,
// 而不只是内核自己跟自己对齐。 // 而不只是内核自己跟自己对齐。
// //
@ -101,9 +101,9 @@ func (p *e2ePlugin) Start(s *sdk.PluginSDK) error {
func (p *e2ePlugin) Stop() error { return nil } func (p *e2ePlugin) Stop() error { return nil }
` `
// procRuntimeTemplates 列出 plugindev 会生成到插件目录的运行时文件。 // procRuntimeTemplates 列出 hmapdev 会生成到插件目录的运行时文件。
// //
// 必须与 SDK 仓 tools/plugindev/proc_runtime.go 的 procRuntimeFiles 一致: // 必须与 SDK 仓 tools/hmapdev/proc_runtime.go 的 procRuntimeFiles 一致:
// 共享段与事件通知的传递机制按平台不同Unix 继承 fdWindows 命名 // 共享段与事件通知的传递机制按平台不同Unix 继承 fdWindows 命名
// 内核对象),故拆成带 build tag 的文件;只写主模板会编译失败。 // 内核对象),故拆成带 build tag 的文件;只写主模板会编译失败。
var procRuntimeTemplates = []struct { var procRuntimeTemplates = []struct {
@ -115,15 +115,21 @@ var procRuntimeTemplates = []struct {
{"proc_shm_windows.go.tmpl", "z_proc_shm_windows.go"}, {"proc_shm_windows.go.tmpl", "z_proc_shm_windows.go"},
} }
// buildPluginWithRealTemplate 用 plugindev 的真实模板编译一个插件二进制。 // buildPluginWithRealTemplate 用 hmapdev 的真实模板编译一个插件二进制。
func buildPluginWithRealTemplate(t *testing.T, businessCode string) string { func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
t.Helper() t.Helper()
if _, err := exec.LookPath("go"); err != nil { if _, err := exec.LookPath("go"); err != nil {
t.Skip("环境无 go 工具链,跳过端到端测试") t.Skip("环境无 go 工具链,跳过端到端测试")
} }
// 工具链在 SDK 1.2.0 起改名 hmapdev原 plugindev。两个目录都接受
// 旧检出(软链或旧版 SDK 仓)仍能跑本测试,新检出走新路径。
tmplDir := filepath.Join("..", "..", "..", tmplDir := filepath.Join("..", "..", "..",
"third_party", "homeagent-sdk", "tools", "plugindev", "templates") "third_party", "homeagent-sdk", "tools", "hmapdev", "templates")
if _, err := os.Stat(tmplDir); err != nil {
tmplDir = filepath.Join("..", "..", "..",
"third_party", "homeagent-sdk", "tools", "plugindev", "templates")
}
dir := t.TempDir() dir := t.TempDir()
mustWriteFile(t, filepath.Join(dir, "plugin.go"), businessCode) mustWriteFile(t, filepath.Join(dir, "plugin.go"), businessCode)
@ -131,7 +137,7 @@ func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
for _, rt := range procRuntimeTemplates { for _, rt := range procRuntimeTemplates {
data, err := os.ReadFile(filepath.Join(tmplDir, rt.tmpl)) data, err := os.ReadFile(filepath.Join(tmplDir, rt.tmpl))
if err != nil { if err != nil {
t.Skipf("plugindev 模板 %s 不可读SDK 仓可能未就位): %v", rt.tmpl, err) t.Skipf("hmapdev 模板 %s 不可读SDK 仓可能未就位): %v", rt.tmpl, err)
} }
mustWriteFile(t, filepath.Join(dir, rt.out), string(data)) mustWriteFile(t, filepath.Join(dir, rt.out), string(data))
} }

View File

@ -260,7 +260,7 @@ func (p *Process) handshake(timeout time.Duration) error {
return fmt.Errorf("proc: %s 握手应答解析失败: %w", p.name, err) return fmt.Errorf("proc: %s 握手应答解析失败: %w", p.name, err)
} }
if res.Protocol != ProtocolVersion { if res.Protocol != ProtocolVersion {
return fmt.Errorf("proc: %s 协议版本不匹配(插件 %d内核 %d——请用配套 plugindev 重编", return fmt.Errorf("proc: %s 协议版本不匹配(插件 %d内核 %d——请用配套 hmapdev 重编",
p.name, res.Protocol, ProtocolVersion) p.name, res.Protocol, ProtocolVersion)
} }
log.Printf("[proc] %s 已建链pid=%d protocol=%d sdk=%s", log.Printf("[proc] %s 已建链pid=%d protocol=%d sdk=%s",

View File

@ -331,7 +331,7 @@ func TestProcess_ProtocolMismatchRejected(t *testing.T) {
} }
// 运维可读性:光报“不匹配”不能定位到行动。生产上碰到它的现场是 // 运维可读性:光报“不匹配”不能定位到行动。生产上碰到它的现场是
// “只更新了内核没重编插件”,所以错误里必须带出这条修复指令。 // “只更新了内核没重编插件”,所以错误里必须带出这条修复指令。
if !strings.Contains(err.Error(), "plugindev") || !strings.Contains(err.Error(), "重编") { if !strings.Contains(err.Error(), "hmapdev") || !strings.Contains(err.Error(), "重编") {
t.Errorf("错误应给出重编插件的修复指令,实际: %v", err) t.Errorf("错误应给出重编插件的修复指令,实际: %v", err)
} }
} }

View File

@ -143,7 +143,7 @@ func AttachSegment(data []byte) (*Segment, error) {
return nil, fmt.Errorf("proc: 共享段魔数不匹配0x%x期望 0x%x", got, shmMagic) return nil, fmt.Errorf("proc: 共享段魔数不匹配0x%x期望 0x%x", got, shmMagic)
} }
if got := binary.LittleEndian.Uint32(data[offVersion:]); got != shmVersion { if got := binary.LittleEndian.Uint32(data[offVersion:]); got != shmVersion {
return nil, fmt.Errorf("proc: 共享段版本不匹配(%d本内核 %d——插件需用配套 plugindev 重编", return nil, fmt.Errorf("proc: 共享段版本不匹配(%d本内核 %d——插件需用配套 hmapdev 重编",
got, shmVersion) got, shmVersion)
} }
return &Segment{data: data}, nil return &Segment{data: data}, nil

View File

@ -4,7 +4,7 @@
// stage 处理经共享内存读改写(模拟 sanitizer 的清洗行为)。 // stage 处理经共享内存读改写(模拟 sanitizer 的清洗行为)。
// //
// 它手写 RPC 与共享段访问,不依赖公开 SDK——因为 SDK 侧的 proc 支持 // 它手写 RPC 与共享段访问,不依赖公开 SDK——因为 SDK 侧的 proc 支持
// 属于 Part 3plugindev 工具链)的内容。这里只验证内核侧机制。 // 属于 Part 3hmapdev 工具链)的内容。这里只验证内核侧机制。
package main package main
import ( import (

View File

@ -1175,11 +1175,11 @@ func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}
// 旧 .so/.dll 插件给明确错误,不静默跳过。 // 旧 .so/.dll 插件给明确错误,不静默跳过。
// 静默跳过会让「插件目录在但没加载」看起来像配置问题, // 静默跳过会让「插件目录在但没加载」看起来像配置问题,
// 而实际原因是需要用新 plugindev 重编。 // 而实际原因是需要用新 hmapdev 重编。
if hasLegacyCABIEntry(plgDir) { if hasLegacyCABIEntry(plgDir) {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"plugin %s: 检测到旧 C ABI 产物plugin.so/.dll/.dylib。"+ "plugin %s: 检测到旧 C ABI 产物plugin.so/.dll/.dylib。"+
"外部插件已改为子进程模式,请用新版 plugindev 重编产出 %s"+ "外部插件已改为子进程模式,请用新版 hmapdev 重编产出 %s"+
"(业务代码无需修改)", name, binEntry) "(业务代码无需修改)", name, binEntry)
} }

View File

@ -23,7 +23,7 @@ import (
// 这里用 **example/ 里真实的 17 个插件产物**,验证「业务代码零改动 + 重编即可」 // 这里用 **example/ 里真实的 17 个插件产物**,验证「业务代码零改动 + 重编即可」
// 这一迁移承诺在完整内核装配下成立。 // 这一迁移承诺在完整内核装配下成立。
// //
// 前置:插件需已用新版 plugindev 重编scripts/rebuild-plugins.sh // 前置:插件需已用新版 hmapdev 重编scripts/rebuild-plugins.sh
// 未重编时测试 skip 而非 fail——CI 上不强制要求先跑重编脚本。 // 未重编时测试 skip 而非 fail——CI 上不强制要求先跑重编脚本。
// realPluginDir 返回某个 example 插件的 linux 产物路径。 // realPluginDir 返回某个 example 插件的 linux 产物路径。