v0.7.2: 根目录清理 + Agent 心跳重构 + 内嵌 ONNX 模型

- 根目录清理: branding/docs/knowledge -> assets/, package/tools/deploy -> deploy/
- meta.go: Version 0.7.2, SDKCompatibleVersion 语义改为最高兼容
- Makefile: 版本回退 0.7.2
- registry.go: 系统提示词改用 meta.Version 格式化
- Agent 心跳: reorgGraph 拆分为三个独立循环(archive/merge/review),各自可配间隔
- GraphDB: 新增 sentences 表 + 关系句子溯源 + ClearSentenceID + CleanupOrphanedSentences
- Knowledge: 支持词嵌入向量化器
- NLP 四阶段流水线: Parse -> Extract -> Verify -> Fuse + SentenceRef
- 移除远程 HTTP 解析器(remote_parser.go)
- 新增内嵌 ONNX 模型(vocab + dep_parser.onnx):
  +build onnxruntime: 全量 ONNX Runtime 推理
  !build onnxruntime: 内嵌词表规则式降级解析器
- config: core.agent.onnx_model_path 替代 dep_parser_url
This commit is contained in:
JianFeeeee
2026-07-28 09:56:26 +08:00
parent 1cb3e87dde
commit 2c5f9ff262
47 changed files with 26141 additions and 242 deletions

120
assets/docs/en/ADAPTER.md Normal file
View File

@ -0,0 +1,120 @@
**中文** | [English](../en/ADAPTER.md)
# Lua Adapter — LLM Source Adaptation Guide
Each LLM API source corresponds to a Lua script, responsible for request transformation (Go unified format → API format) and response transformation (API format → Go unified format).
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Adapter Contract
The Lua script must return a table containing the following fields and functions:
```lua
local adapter = {}
-- Metadata
adapter.name = "my_provider" -- Unique identifier, matches adapter field in config
adapter.version = "2.0.0"
adapter.endpoint = "/v1/chat/completions" -- API path, appended to base_url
adapter.headers = {} -- Additional HTTP request headers
-- Request transformation: Go → API
function adapter.transform_request(raw_json)
-- raw_json: Go's CompletionRequest JSON string
-- Returns: JSON string to send to API
return transformed_json
end
-- Response transformation: API → Go
function adapter.transform_response(raw_json)
-- raw_json: API's raw response JSON string
-- Returns: Unified CompletionResponse JSON string
-- Unified format:
-- { content: "", finish_reason: "", token_usage: { prompt: N, completion: N, total: N }, tool_calls?: [...] }
return unified_json
end
-- Stream chunk transformation (optional)
function adapter.transform_stream_chunk(raw_line)
-- raw_line: JSON string after data: in SSE
-- Returns: JSON of { content: "", done: bool }, return "" to skip this chunk
return chunk_json
end
return adapter
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Unified CompletionRequest Format (Go → Adapter)
```json
{
"model": "deepseek-v4-flash",
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "...", "tool_calls": [...] }
],
"temperature": 0.7,
"max_tokens": 4096,
"stream": false,
"tools": [...],
"tool_choice": "auto"
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Unified CompletionResponse Format (Adapter → Go)
```json
{
"content": "Response content",
"finish_reason": "stop",
"token_usage": { "prompt": 10, "completion": 20, "total": 30 },
"tool_calls": [
{ "id": "call_xxx", "type": "function", "name": "tool_name", "arguments": { "key": "val" } }
]
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Lua VM Built-in Functions
`json.encode(table)` — Encode Lua table to JSON string
`json.decode(string)` — Decode JSON string to Lua table
`log(level, message)` — Output log (level: info/warn/error)
`http_get(url)` — Perform HTTP GET request, returns response body as string
`http_post(url, body)` — Perform HTTP POST request, returns response body as string
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Adapting Typical APIs
| API | endpoint | auth method | Format differences |
|-----|----------|-------------|-------------------|
| **OpenAI** | `/chat/completions` | `Authorization: Bearer <key>` | Standard OpenAI format |
| **DeepSeek** | `/chat/completions` | `Authorization: Bearer <key>` | OpenAI compatible, forces temperature=0 |
| **Anthropic** | `/v1/messages` | `x-api-key: <key>` | Messages API, system message separated, content as block array |
| **Gemini** | `/v1/models/{model}:generateContent` | `?key=<key>` or Bearer | contents/parts format, role uses model instead of assistant |
| **Mistral** | `/v1/chat/completions` | `Authorization: Bearer <key>` | OpenAI compatible |
| **Groq** | `/openai/v1/chat/completions` | `Authorization: Bearer <key>` | OpenAI compatible |
| **GitHub Models** | `/chat/completions` | `Authorization: Bearer <pat>` | OpenAI compatible |
| **Ollama** | `/api/chat` | None | Different options format |
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Steps to Add a New Source
1. Create `<name>.lua` under `internal/lua/adapters/`
2. Script defines `transform_request` and `transform_response`
3. (Optional) Define `transform_stream_chunk` for streaming support
4. Build verification: `go build ./cmd/homed/`
5. Test verification: `go test ./...`

View File

@ -0,0 +1,441 @@
**中文** | [English](../en/ARCHITECTURE.md)
# HomeAgent Architecture
## Architectural Principles
HomeAgent's cognitive architecture consists of three subsystems: the event loop (eventLoop), the context window (RelevanceContext), and the stage pipeline (StageHost). Together they form the orchestration framework. Within this framework, the LLM serves as a scheduled reasoning unit; cognitive continuity is maintained by the event loop, context window, and stage pipeline.
**The event loop (eventLoop)** is a three-way select: `a.io.InputChan()` receives external user input and dispatches to `processTextInput` / `processMediaInput`; `a.selfInputCh` receives internal system tasks (memory merges, distillation callbacks) routed through `processConsolidation` under the `_consolidation_` output channel; `a.ctx.Done()` accepts shutdown signals. A concurrently running `interceptLoop` goroutine independently reads `a.io.InputInterruptChan()` — on receiving a high-priority interrupt, it cancels the in-flight LLM HTTP request (`a.cancelLLM()`), then writes the event to `a.interceptCh`. This channel is drained non-blockingly by `drainInterrupts()` before each LLM call in `process()`, injecting interrupts as `[打断消息]` formatted entries into message history. The three interrupt delivery paths carry distinct semantics: `cancelLLM` terminates the current HTTP request, `interceptCh` injects text before the next LLM turn, and `InjectInput` triggers a new processing cycle when the event loop is idle.
**The stage pipeline (StageHost)** manages two registration categories: tool definitions (ToolDef) and stage handlers (StageHandler). ToolDef includes two optional memory control fields: `NoMemory bool` — when true, the tool's output is excluded from vectorization/jieba/distillation (original text preserved); and `Cleaner func(string) string` — a filter applied before the output enters the computation layer (e.g., extracting a `content` field from JSON). Neither modifies the original output; both only affect the computation layer input. `RegisterTool` rejects duplicate names, infers the owning plugin name from the tool name prefix, and maintains a `toolPlugins` mapping. `RegisterStage` appends handlers to the corresponding stage list. On stage execution (`RunStage`), **all registered handlers execute in parallel via goroutines**, sharing a single `*StageContext` protected by `sync.RWMutex`. Individual handler panics are recovered independently without affecting other handlers. Short-circuit semantics are implemented by checking `ctx.Response != nil` — any stage handler can set this value to terminate the pipeline early. `ExecuteTool` includes built-in panic recovery with stack-trace recording. `UnregisterPluginTools` removes a plugin's tool set during hot-reload.
**The context window (RelevanceContext)** maintains a chronologically ordered event list. `Append` applies `CleanTemplateText` to strip QQ templates and timestamp noise before computing the embedding vector using a three-branch strategy (agent events use Response, user events use Input, cold_storage uses Input+Response). `Prune` triggers when the event count exceeds `topK`: it **unconditionally protects the last 10 events from eviction** (recency bias), scores remaining candidates against the current input via CosineSimilarity, keeps `topK - 10` highest-scoring entries (floor at 0), then re-sorts chronologically. Pruned events from sources other than `agentcli` and `terminal` are archived to the Document layer via `docStore.ContextToDoc`, retaining original timestamps. Persistence uses 5-second debounced writes to a JSON file.
**Tool definitions are aggregated from five sources**: IOManager-registered plugin tools; StageHost-registered SDK tools; Indexer-provided memory index tools; conditionally added built-in tools (depending on non-nil state of memory/knowledge/docStore/social/pluginReg/providerManager modules — including memory operations, knowledge retrieval, document queries, social networking, plugin reloading, child-agent spawning, per-output-channel send tools, and LLM source switching); and media processing tools added based on `pendingMedia` state. `buildToolDefs()` re-aggregates all sources on each process cycle.
**Provider invocation follows an ordered fallback strategy**: `ProviderManager.OrderedProviders()` returns the provider list in registration order. The `process()` inner loop iterates this list attempting `Chat()` on each. HTTP 401/403 responses mark the provider as permanently unavailable; other error types also mark unavailability but with higher tolerance. If all providers fail, an error is returned to the caller. If a call is interrupted by context cancellation while the agent is still running, it is retried (only on non-consolidation paths).
**The memory system adopts a three-tier storage hierarchy (Context → Document → Graph), tiering data by access locality and persistence requirements**: the Context layer is a fast-volatile working window using StaticEmbedder (pretrained word embeddings with TF-IDF fallback) for semantic relevance scoring; the Document layer **shares the same StaticEmbedder vector space with Context** (the embedder is injected into the Document Store at agent startup via `docStore.SetVectorizer(embedder)`), ensuring that relevance scores during Context pruning and semantic retrieval during Document queries operate within the same vector space — TF-IDF serves only as a fallback when the embedder is unavailable; the Graph layer uses SQLite as its persistence substrate with an entities table (nodes) and a relations table (directed edges), supporting BFS traversal recall. Data migration policies govern movement across tiers: low-scoring events sink from Context to Document (vectorized using the same embedder at archival time); cold documents, after a 72-hour no-access threshold, are distilled into triples via `docToTriples` and committed to Graph. The Indexer uses dual retrieval (entity vector similarity search + jieba keyword extraction) to construct Graph query seeds, and the `MarkRecalled` mechanism prevents entities already fetched via tool calls from being re-injected into the system prompt.
**The separation of core domain and application domain** constrains the kernel's responsibilities to LLM orchestration, memory management, and knowledge retrieval — no direct IO operations; all external interaction is mediated through the plugin domain. This separation limits the kernel's complexity to a verifiable scope while granting the plugin domain independent evolution: plugins can be independently developed, independently released, hot-loaded, and do not directly affect the stability of the core domain.
## Message Processing Flow
### Full Pipeline
```
External input (via plugin InjectInput)
eventLoop() → processTextInput()
├── on_input stage Plugins can intercept/rewrite/short-circuit
├── Context.Append Record to context window
├── Context.Prune Low-relevance events archived to Document
├── buildMemoryContext() Indexer recall → GraphDB BFS traversal
├── pre_action stage Plugins can inject system messages
├── [Tool Loop] process()
│ ├── buildSystemPrompt Persona + Memory + Knowledge + Context
│ ├── buildToolDefs Built-in tools + Plugin tools
│ ├── provider.Chat() LLM call
│ ├── post_action stage Plugins see LLM output + tool list
│ ├── Has tools?
│ │ ├── before_toolcall Plugins can reject/modify params
│ │ ├── executeToolCall Route to plugin/built-in
│ │ ├── after_toolcall Plugins can modify results
│ │ └── → back to post_action
│ └── No tools → exit loop
├── Context.Append(response)
├── before_output stage Plugins can modify final text
├── emitResponse() Send via output_send
└── after_output stage Read-only, cleanup
```
Code: `internal/agent/core/process.go``process()` is the main tool loop
### 7 Stage Hooks
| Stage | Trigger | Plugin Capabilities |
|-------|---------|---------------------|
| `on_input` | Message arrives at Agent, zero processing | Blacklist/rate-limit/short-circuit reply |
| `pre_action` | Context ready, before LLM call | Inject external data into context |
| `post_action` | LLM returns text + tool list | Sensitive word filter/forced redirect |
| `before_toolcall` | Before single tool execution | Audit/reject/modify params |
| `after_toolcall` | After single tool execution | Desensitize/sort results |
| `before_output` | Final text ready, before sending | Format adaptation |
| `after_output` | Already sent | Statistics/logging |
Code: `internal/agent/core/stages.go``StageHost` orchestration
### Loop Rules
`post_action → [before_toolcall → execute → after_toolcall] → post_action` forms the inner loop.
Exit conditions: LLM has no tool calls / all rejected / exceeded limit.
### Short-Circuit Rules
Setting `ctx.Response` at any stage jumps to `after_output`.
## Three-Layer Memory
### Memory Flow
```
① Context (Working Window)
RelevanceContext — In-memory events[] + JSON persistence
Append: Each input, CleanTemplateText → three-branch vector(textForVector)
agent→Response, user→Input, cold_storage→Input+Response
StaticEmbedder pretrained word embedding / TF-IDF fallback
Prune: StaticEmbedder CosineSimilarity, keep topK + last 10
├── Keep → timeline → chronologically sorted → system prompt
└── Low score → Document layer archive (original timestamp)
Save: 5s debounce write to disk
↓ Prune archive ↑ LLM active recall
② Document (File Memory)
DocStore — JSON files + shared StaticEmbedder vector space with Context (fallback: TF-IDF InvertedIndex)
Write: Prune archive / doc_commit / Graph snapshot (syncGraphToDocs)
Read:
├── Auto-inject: Query(input, top3) → similarity summary under same vector space → [Related Memory Docs] → system prompt (read-only)
└── LLM active: doc_query → Consume(read and delete)
→ context.Append{Timestamp: d.CreatedAt, Source: "cold_storage"} per doc
→ Docs written to context timeline with original timestamps, deleted from docStore
Cold: FindColdDocs(72h, ≤2 accesses) → docToTriples → Graph
↓ Cold doc distillation ↑ Auto recall
③ Graph (Graph Database)
SQLite — entities + relations tables
Write: memory_commit / cold doc distillation / Pipeline rule distillation / memory_merge
Read:
├── Auto recall: Indexer.BuildContext(input)
│ → CleanTemplateText → vector entity search + jieba keywords → SQLite LIKE + BFS depth=2
│ → [Memory Index] → system prompt
└── LLM active: memory_recall / memory_merge / memory_purge / memory_edit / memory_delete_entity
Social: person_query / set_trait / relate (wraps GraphDB)
④ Distillation Pipeline (30min heartbeat)
distillContext → window > 2×maxSize → force Prune
syncGraphToDocs → Graph snapshot to Document (cross-layer searchable)
reorgGraph:
Step1: indexer.Sync — rebuild entity vector index
Step2: docStore.Reindex — rebuild document vector index
Step3: Cold docs → docToTriples → GraphDB.Commit
Step4: Entity similarity (Bigram Jaccard > 0.75) → consolidation → LLM decides merge
Step5: evaluateGraphQuality → LLM decides keep/delete
⑤ Pipeline Rule Distiller (every heartbeat)
distillOnce → regex match personal info:
我叫X / 我住在X / 我喜欢X / 我X岁 / 我的工作是X
→ triples → GraphDB.Commit
```
### Vectorization: Pretrained Word Embedding + TF-IDF Fallback
All vectorization unified under `StaticEmbedder` (`internal/memory/static_embedder.go`):
**Primary Strategy — Pretrained Word Embedding (aligned 300d)**
- Model sources: ConceptNet Numberbatch (77-language aligned) / fastText Chinese / fastText English
- 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
- Falls back to ConceptNet by default if no match
- **Pre-processing**: `CleanTemplateText` strips QQ tool-call templates and timestamp noise
- **Three-branch vector source**: agent→Response, user→Input, cold_storage→Input+Response
- **TF-IDF fallback**: auto-fallback to bag-of-words TF-IDF if model download fails or not configured
| Location | File | Purpose | Algorithm |
|----------|------|---------|-----------|
| Context Prune | `context.go:155` | Trim low-relevance context events | VectorizeClean → CosineSimilarity(queryVec, evt.Vector) |
| DocStore Query | `document.go:206` | Recall from document memory | StaticEmbedder.Vectorize (primary) / TF-IDF (fallback) → vec.Search |
| Indexer Entity Search | `indexer.go:96+111` | Recall from Graph | vector entity search + jieba keywords → SQLite LIKE + BFS |
| Entity Similarity Detection | `distill.go` | Detect similar entities in Graph | Bigram Jaccard (>0.75 → consolidation) |
### Context Layer
`internal/agent/core/context.go``RelevanceContext`
- Maintains recent event list, writes JSON on each Append/Prune to prevent data loss
- Pre-vectorization pipeline runs through `CleanTemplateText` to remove template noise
- Three-branch `textForVector`: agent events → Response, user events → Input, cold_storage → Input+Response
- Pretrained word embedding `StaticEmbedder` → CosineSimilarity, auto-fallback to TF-IDF if unavailable
- Protects last 10 events from eviction; excess candidates are sorted by relevance and archived to document memory
- Archived events retain original timestamps; on `doc_query` recall they re-insert into the context timeline at their original position
### Document Layer
`internal/memory/document/document.go``Store`
- Consume-on-read mode: deleted after `doc_query` retrieval
- Dual recall: shared StaticEmbedder semantic vector search + jieba keyword extraction (falls back to char-bigram TF-IDF when model is not loaded)
### Graph Layer
`internal/memory/graph.go``GraphDB`
- SQLite WAL mode, two tables (driver: mattn/go-sqlite3, CGo)
- `Commit(triples)` — UPSERT entities + INSERT relations
- `Recall(keywords, depth)` — Keyword LIKE search + BFS traversal
### Memory Tools (LLM-callable)
| Tool | Purpose |
|------|---------|
| `memory_recall` | Recall from Graph |
| `memory_commit` | Write triples to Graph |
| `memory_merge` | Merge two entity nodes |
| `memory_purge` | Delete entity node |
| `memory_edit` | Edit existing entity/relation |
| `memory_delete_entity` | Delete entity and all its relations |
| `memory_introspect` | View memory statistics |
| `doc_query` | Search from Document |
| `doc_commit` | Write to Document |
### Other Memory Layers
- **Social** (`internal/memory/social/social.go`) — Persona traits and relationship network, wraps GraphDB entity types
- **Text Memory** (`internal/memory/text/text.go`) — Raw conversation JSONL logs, rotation strategy
- **Memory Indexer** (`internal/memory/indexer.go`) — Entity vectorization + jieba keyword extraction, auto-inject into system prompt
### Distillation Pipeline
`internal/memory/pipeline/pipeline.go`
- 10-minute tick, 7-day retention
- Rule-based triple extraction (name / location / likes / age / job patterns)
- Writes to GraphDB
### Context Pruning
```
Heartbeat 30min:
├── distillContext() — Distill current context
├── syncGraphToDocs() — Graph → Document sync
└── reorgGraph()
├── Indexer.Sync()
├── DocStore.Reindex()
├── Cold docs → Graph
└── Entity conflicts → enqueueConsolidationTask()
selfInputCh → LLM decides merge/skip
```
Entity conflict detection heuristic (bigram Jaccard > 0.75), routed through `selfInputCh` internal channel, LLM makes the final merge decision.
## Knowledge Base
`internal/knowledge/knowledge.go`
- File directory `knowledge/<name>/content.md`
- Independent TF-IDF index, separate from memory system
- `knowledge_search` / `knowledge_create` / `knowledge_list`
## Provider & Lua Adapter Layer
```
Agent
Provider Interface (Name / Chat / ChatStream)
├── OpenAIProvider — Standard OpenAI API
├── OllamaProvider — Local Ollama
└── LuaAdaptedProvider (primary)
├── Serialize CompletionRequest → JSON
├── adapter.transform_request() → API format
├── HTTP request + adapter.headers
├── adapter.transform_response() → unified format
└── Deserialize
```
Code: `internal/agent/api/provider.go`
ProviderManager manages multiple sources, fallback in registration order. Lua adapters at `internal/lua/adapters/`, each `.lua` script defines `transform_request` / `transform_response` / `transform_stream_chunk`.
VM built-ins: `json.encode` / `json.decode` / `log` / `http_get` / `http_post`.
## Plugin System
### Four Loading Methods
| Method | Registration Mechanism | Compilation | Usage |
|--------|----------------------|-------------|-------|
| Built-in | `init()``RegisterFactory` | `internal/plugins/` compiled into kernel | webui/cli/timer/mcp etc. |
| External `.so` | C ABI dynamic loading | `-buildmode=c-shared` + bridge | qq/files/web/memo etc. |
| Lua script plugin | Parse `main.lua` to register tools | No compilation, hot-reload | luaplugintest/testlua etc. |
| SKILL plugin | Parse `SKILL.md` | Markdown definition | Loaded via clawhubadapter |
Built-in plugin registration: `internal/plugins/all.go` blank imports → each plugin `init()``Registry.Load()` scans directory to match factory.
External plugin loading: `internal/plugin/dynamic.go` → copy to SHA256 temp path (bypass `plugin.Open` path cache) → `Open` + `Lookup("NewPlugin")`.
Lua script plugin loading: `internal/lua/` → parse `main.lua` via Lua VM, call `start()` to register tools.
### PluginSDK Four Channels
```
Plugin ──→ Kernel
RegisterTool(name, fn) ──→ buildToolDefs() / executeToolCall()
RegisterStage(stage, fn, scope...) ──→ runStage() called at corresponding phase (scope: global / own-tools-only)
Subscribe(event, fn) ──→ Publish() notify all subscribers
RegisterOutputChannel(name, caps, desc, handler) ──→ output_send__{name} tool generation
```
`internal/sdk/` bridges external SDK interface to kernel, defines complete PluginSDK:
```go
sdk.RegisterTool(name, def, handler)
sdk.RegisterStage(stage, handler, scope...)
sdk.Publish(event)
sdk.InjectInput(source, channel, payload)
sdk.InjectInterrupt(source, channel, payload)
sdk.Memory().Recall/Commit
sdk.Knowledge().Search/Create
sdk.Settings().Get/Set/List
sdk.RegisterOutputChannel("qq", sdk.CapText|sdk.CapAudio|sdk.CapImage, "QQ channel, see output_send__qq_help for details", handler)
```
### Plugin Interface
```go
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
```
## Output Channel System
Each output channel generates two tools:
| Tool | Type | Purpose |
|------|------|---------|
| `output_send__{name}` | function | Accepts `payload` (content), `meta` (JSON routing metadata), `type` (enum) — routed to plugin handler |
| `output_send__{name}_help` | function | Returns the channel's meta format and type enum documentation |
Capability flags:
| Flag | Value | Meaning |
|------|-------|---------|
| CapText | 1 | Plain text |
| CapFile | 2 | File |
| CapImage | 4 | Image |
| CapAudio | 8 | Audio |
| CapStructured | 16 | Structured data |
System prompt injection: output gate rules, multi-call support, long message splitting.
Child agent permission: `output_send__` prefix tools are allowed.
## EventAgentLLMChain Event
- Event type `agent_llm_chain` emitted after each LLM turn
- Contains the full LLM response (text + tool calls + reasoning)
- WebUI subscribes to this event via SSE for real-time display
- Plugins can subscribe via EventSubscriber (read-only for external plugins)
## Restricted External Plugin API
Layered architecture: internal plugins get full PluginSDK, external plugins get restricted SDK.
| API | Internal Plugin | External Plugin |
|-----|-----------------|-----------------|
| SocialAPI | Full read/write | Read-only (GetPerson / GetTrait / GetRelations / GetNetwork / ListPersons) |
| EventSubscriber | Subscribe + Publish | Subscribe-only (no Publish capability) |
Extended fields:
- Triple extensions: Confidence, SubjectType, ObjectType
- Relation extension: Confidence
## Interrupt Mechanism
```
interceptLoop (goroutine)
├── InputInterruptChan() ← Timer/message notifications
├── (a) cancelLLM() → Cancel Provider HTTP request
├── (b) interceptCh → process() pre-loop read [interrupt message]
└── (c) InjectInput() → Trigger new processing when idle
```
Three delivery paths:
| Path | Effect | Timing |
|------|--------|--------|
| cancelLLM | Cancel current HTTP request | On context.Canceled |
| interceptCh | Insert `[interrupt message]` in process() | Before each LLM call |
| InjectInput | Trigger new processing when eventLoop is idle | No ongoing request |
Code: `internal/agent/core/eventloop.go``interceptLoop` / `drainInterrupts`
## Configuration System
`internal/config/registry.go` — ConfigRegistry
- SQLite storage, `config` table + `config_<plugin>` independent tables
- Namespaces: `core.*` / `plugin.<name>.*`
- `RegisterDefault` inserts ~80 default keys (seeds for 8 LLM sources)
- WebUI settings page `/api/v1/settings` for read/write
## Code Structure
```
cmd/homed/main.go — Entry: assembles all subsystems
cmd/waiter/main.go — CLI client (Unix socket)
internal/
├── agent/
│ ├── core/ — Agent core (eventLoop/process/stages/context)
│ │ └── plugin_health.go — Plugin health monitoring and auto-restart
│ ├── api/ — Provider interface + LuaAdaptedProvider
│ ├── io/ — IOManager (queue/interrupt/output)
│ └── personal.go — Persona loading
├── plugin/
│ ├── registry.go — Registry + lifecycle
│ ├── dynamic.go — .so dynamic loader
│ └── manifest.go — plugin.json metadata
├── plugins/ — Built-in plugin implementations
│ ├── all.go — Blank imports
│ ├── webui/ — HTTP server + embedded SPA
│ ├── cli/ — Unix socket CLI
│ ├── timer/ — Timer
│ ├── cmd/ — Command execution
│ ├── mcp/ — MCP protocol
│ ├── files/ — File operations
│ ├── clawhubadapter/ — ClawHub adapter (OC plugin/SKILL/JS/Python sidecar)
│ ├── agentcli/ — PTY terminal
│ ├── healthcheck/ — Health check
│ ├── pluginmgr/ — Plugin manager
│ └── cfgmgr/ — Config manager
├── sdk/ — PluginSDK definitions
│ ├── plugin.go — Plugin interface + PluginSDK
│ ├── memory.go — MemoryAPI
│ ├── knowledge.go — KnowledgeAPI
│ ├── settings.go — SettingsAPI
│ └── llm.go — LLMAPI
├── memory/
│ ├── graph.go — SQLite graph database
│ ├── indexer.go — Graph → vector index
│ ├── vector/store.go — TF-IDF vector engine
│ ├── document/document.go — Document memory
│ ├── text/text.go — Text logs
│ └── pipeline/ — Distiller
├── knowledge/knowledge.go — Knowledge base
├── lua/
│ ├── vm.go — Lua VM (json/log/http)
│ └── adapters/ — 8 LLM adapter scripts
├── config/registry.go — SQLite config center
├── events/bus.go — Event bus
├── tracker/ — OverlayFS change tracking
├── supervisor/ — Daemon management
├── skill/ — Skill plugin management
│ └── manager.go — Skill loading/matching
└── meta/ — Meta information
└── meta.go — Agent metadata
```

View File

@ -0,0 +1,83 @@
**中文** | [English](../en/OVERVIEW.md)
# HomeAgent — Project Overview
## What Is This
HomeAgent is a continuously-running personal intelligent Agent framework.
Core architecture: a long-running kernel process (`homed`) that connects to various IO channels (QQ, Web, CLI, etc.) through a plugin system. The kernel handles LLM orchestration, memory management, and knowledge retrieval; plugins handle all external IO — sending/receiving messages, file operations, web search, etc.
### Design Highlights
**Separation of Core Domain and Application Domain** — The kernel (core domain) performs no IO operations; all IO capabilities belong to plugins (application domain). The boundary is defined through PluginSDK:
- Plugins register tools (Tool) with the kernel for LLM invocation
- Plugins hook into the processing pipeline (Stage) to intercept/rewrite message flow at various phases
- Plugins subscribe/publish events (Event) for loosely-coupled communication
- Plugins queue or interrupt input delivery through IO API
The significance lies in clear responsibility boundaries: the kernel focuses on orchestration and memory management, while plugins handle IO implementation — the two are not coupled.
**Three-Layer Memory Architecture** — Manages information retention across long agent runtimes through a tiered storage strategy:
- **Context Layer**: In-memory local word embedding scored event window (jieba + TF-IDF + PMI → CosineSimilarity), maintains recent context in real-time, low-relevance events automatically sink to the next layer
- **Document Layer**: JSON files + TF-IDF vector-indexed temporary memory, supports explicit submission and implicit archival, cold data distills to Graph
- **Graph Layer**: SQLite graph database, persists entities and relations, BFS traversal recall, distillation pipeline extracts triples from conversations
Three progressive layers — context, cold archive, long-term graph memory — form an information decay and consolidation pipeline from short-term to persistent storage.
## What It Actually Does
Code is in the project root, implemented in Go.
**Kernel** (`internal/agent/core/`):
- `eventloop.go` — Message loop (`eventLoop`), queuing input from the IO layer
- `process.go` / `stages.go` — Processing pipeline: memory recall → persona injection → LLM call → tool execution → output delivery, 7 stage hooks
- `toolcall.go` — Tool scheduling and execution
- `context.go` — Context management (pretrained word embedding scoring StaticEmbedder → CosineSimilarity, TF-IDF fallback), automatic pruning of low-relevance events
- LLM calls abstracted through Provider interface, supports 8 LLM sources with automatic fallback
**Memory System** (`internal/memory/`):
- **GraphDB** (`graph.go`) — SQLite, entities + relations tables, BFS traversal
- **Document Store** (`document/document.go`) — Temporary memory, JSON files + TF-IDF vector index, consume-on-read
- **Text Memory** (`text/text.go`) — Raw conversation logs, JSONL file rotation
- **Social Store** (`social/social.go`) — Persona traits + relationship network, wraps GraphDB
- **Memory Indexer** (`indexer.go`) — Auto-vectorizes GraphDB entities, recalls and injects into system prompt on user input
**Knowledge Base** (`internal/knowledge/knowledge.go`):
- File system directory `knowledge/<name>/content.md`
- TF-IDF vector search, independent index instance from the memory system
- LLM operates via three tools: `knowledge_search` / `knowledge_create` / `knowledge_list`
**Plugin System** (`internal/plugin/`):
- Built-in plugins: Go `init()` self-registration, compiled into kernel
- External plugins: Go `-buildmode=c-shared` compiled to `.so`, dynamically loaded via C ABI bridge; also supports Lua script plugins
- PluginSDK (`internal/sdk/`) defines four channels: RegisterTool / RegisterStage / Subscribe / RegisterOutputChannel
- 7 stage hooks: on_input → pre_action → post_action → before_toolcall → after_toolcall → before_output → after_output
**LLM Provider** (`internal/agent/api/provider.go`):
- Provider interface: Name / Chat / ChatStream
- Three implementations: OpenAIProvider (standard OpenAI API), OllamaProvider (local), LuaAdaptedProvider (Lua adapter)
- LuaAdapter located at `internal/lua/adapters/`, each LLM source has a corresponding `.lua` script
- 8 built-in adapters: deepseek / openai / anthropic / gemini / mistral / groq / github / ollama
**WebUI** (`internal/plugins/webui/`):
- Embedded SPA dashboard (`dashboard.html` packaged via `//go:embed`)
- REST API: status query, configuration management, memory operations, knowledge management, plugin management
- OpenAI API-compatible `/v1/chat/completions` endpoint
- SSE event stream `/api/v1/chat/events`
**ClawHub Adapter** (`internal/plugins/clawhubadapter/`):
- Unified loader for OC plugins (Node.js), Python sidecar, JS sidecar, and SKILL plugins
- RegistryDispatcher pattern: routes registration notifications to Tool/Provider/Channel/Stage registries
- ClawHub marketplace search and install: `clawhubadapter_search` / `clawhubadapter_npm_install`
- 9 provider types mapped to LLM-accessible tools (image generation, web search, speech, etc.)
- OC channels auto-registered as IO devices with text/file/image/audio capability flags
## Project Status
Core functionality is operational. Plugin system and SDK are ready for independent external plugin development.
- Built-in plugins: webui / cli / timer / cmd / mcp / agentcli / healthcheck / pluginmgr / clawhubadapter / files / cfgmgr
- External plugin examples ([homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repo `example/`, both Go and Lua types): qq / files / web / memo / bili / editdoc / a2a / ocr / sanitizer / luaplugintest / testlua
- Distribution: `.hmap` plugin package format, installable via WebUI

View File

@ -0,0 +1,634 @@
**中文** | [English](../en/PLUGIN_DEV.md)
# HomeAgent Plugin Development Guide
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Overview
All external interaction capabilities of HomeAgent comes from plugins. Plugins interact with the kernel through `PluginSDK` (Go API).
**SDK Repository**: Plugin development tools, template code, and example plugins are hosted in the [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) repository.
```bash
git clone https://gitcode.com/JianFeeeee/homeagent-sdk.git
cd homeagent-sdk
```
Each plugin implements a three-method interface:
```go
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
```
### Three Development Methods
| Method | Use Case | Complexity |
|--------|----------|------------|
| **Dynamic .so/.dll plugin (recommended)** | Independently distributed third-party plugins | Medium, generated using `plugindev` toolchain |
| **Built-in plugin** | Released with HomeAgent | Simple, requires merging into main repo |
| **Lua script plugin** | Lightweight rapid prototyping | Simple, generated using `plugindev init --lua` |
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 1. Quick Start: Using the plugindev Toolchain
`plugindev` is the unified plugin development toolchain provided in the SDK repository, supporting both Go and Lua plugin types.
### Installation
```bash
cd homeagent-sdk/tools/plugindev
go build -o plugindev
# Add plugindev to PATH or use directly
```
### SDK Version Management
`plugindev sdk` manages local SDK versions:
```bash
plugindev sdk list # list installed SDK versions
plugindev sdk current # show current SDK version
plugindev sdk latest # show latest available version
plugindev sdk install v0.7.1 # install a specific version
plugindev sdk use v0.7.1 # switch to a version
plugindev sdk path # show current SDK path
```
SDK is stored at `~/.homeagent/plugindev/sdk/<version>/`; `plugindev init` reads the current SDK version for `go.mod`.
### Creating a Go Plugin
```bash
plugindev init myplugin
cd myplugin
# Edit plugin code
vim plugin.go
# Build and package
plugindev build
# Output: dist/myplugin_linux_amd64.hmap (or windows_amd64)
```
### Creating a Lua Plugin
```bash
plugindev init myluaplugin --lua
cd myluaplugin
# Edit plugin code
vim main.lua
# Local test
lua main.lua
# Build and package
plugindev build
# Output: dist/myluaplugin_lua.hmap
```
### Template Project Structure
**Go plugin**:
```
myplugin/
├── plg.json — Plugin metadata (name, version, entry, target platforms)
├── plugin.go — Plugin implementation (Plugin interface + NewPlugin export)
├── go.mod — Go module definition
├── README.md — Documentation
└── thirdpart/ — Optional external source code directory
```
C ABI bridge files (`z_bridge_gen.go` + `z_entry.c`) are auto-generated at build time.
**Lua plugin**:
```
myluaplugin/
├── plg.json — Plugin metadata (entry: "main.lua", targets: "lua")
├── main.lua — Plugin implementation (Lua version of Plugin interface)
├── sdk.lua — SDK mock layer (supports `lua main.lua` standalone testing)
└── README.md — Documentation
```
### Build & Package
`plugindev build` automatically handles compilation and packaging:
```bash
cd myplugin
plugindev build
```
Execution process:
1. Reads `plg.json` `targets` field to determine target platforms
2. Auto-generates C ABI bridge code (`z_bridge_gen.go` + `z_entry.c`)
3. **Go plugin**: Runs `go build -buildmode=c-shared` (produces `.so` / `.dylib` / `.dll`)
4. **Lua plugin**: Packages source code directly, no compilation needed
5. Generates `plugin.json` output manifest
6. Packages as `.hmap` distribution (zip format, containing `plugin.json` + binary)
### plg.json (project config) vs plugin.json (output manifest)
| File | Purpose | Key fields |
|------|---------|------------|
| `plg.json` | Project metadata, maintained by developer | `targets` — build targets (e.g. `"linux/amd64,windows/amd64"`) |
| `plugin.json` | Build artifact manifest, auto-generated | `entry` — entry filename; `platforms` — declared platforms |
Each target produces a separate `.hmap`; binary name by platform:
| Platform | Binary |
|----------|--------|
| Linux | `plugin.so` |
| macOS | `plugin.dylib` |
| Windows | `plugin.dll` |
### Multi-platform bundle: --bundle
```bash
plugindev build --bundle
```
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.
Output in `dist/` directory:
```
dist/
├── myplugin_linux_amd64.hmap # Single platform: Linux
├── myplugin_windows_amd64.hmap # Single platform: Windows
├── myplugin_darwin_amd64.hmap # Single platform: macOS
├── myplugin_bundle.hmap # Multi-platform bundle
└── myplugin_lua.hmap # Lua plugin
```
### Deployment
Install via PluginMgr HTTP API (three methods):
```bash
# 1. Install from URL (auto-cleanup)
curl -X POST http://127.0.0.1:9876/plugins \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/myplugin.hmap"}'
# 2. Install from local path (keeps source file)
curl -X POST http://127.0.0.1:9876/plugins \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/myplugin.hmap"}'
# 3. Upload binary directly
curl -X POST http://127.0.0.1:9876/plugins \
--data-binary @dist/myplugin.hmap
```
Reload plugins via `/api/v1/plugins/reload` or restart the kernel to activate.
Or upload via WebUI plugin management page.
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 2. Go Plugin Development in Detail
### Plugin Interface
```go
package main
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
type Plugin struct {
name string
sdk *sdk.PluginSDK
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
// Register config items, tools, stage hooks, etc.
return nil
}
func (p *Plugin) Stop() error {
// Clean up resources
return nil
}
// NewPluginFactory creates plugin instance (called by main.go or Windows bridge)
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
```
### Entry Point
`plugindev init` generates `plugin.go` with the `NewPlugin` export function directly,
which is the entry point when the kernel loads the plugin:
```go
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
```
At build time, `plugindev build` auto-generates C ABI bridge code (`z_bridge_gen.go` + `z_entry.c`),
shared by both Windows DLL and Linux/macOS .so builds. No manual bridge code needed.
### PluginSDK Core API
#### Tool Registration — Make your capabilities callable by LLM
```go
s.RegisterTool("weather_query", sdk.ToolDef{
Name: "weather_query",
Description: "Query weather for a specified city",
NoMemory: false, // false=output participates in memory, true=skip
// Cleaner: func(output string) string { // Optional: clean output before vector/jieba/distill
// return extractJSON(output, "content")
// },
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"description": "City name, e.g. Beijing",
},
},
"required": []string{"city"},
},
}, func(args map[string]interface{}) (interface{}, error) {
city, _ := args["city"].(string)
return map[string]interface{}{
"city": city,
"temp": 25,
"weather": "Sunny",
}, nil
})
```
##### NoMemory and Cleaner
`NoMemory` and `Cleaner` are optional fields on `ToolDef` that control how tool output participates in the **memory computation layer** (vectorization, jieba tokenization, distillation):
- **`NoMemory`** (default `false`): When `true`, the tool's output is excluded from all memory computation (vector, tokenization, distillation), but the original text is preserved in Context and Document. LLM attention is unaffected. Use cases: `cmd_run` (unpredictable noise in command output), pure operation tools like file upload/delete.
- **`Cleaner`** (optional): A function `func(output string) string`. When set, the tool output is filtered through this function before participating in vectorization/jieba/distillation. Typical use: stripping SQL prefixes, extracting a `content` field from JSON. The original output is never modified — Cleaner only affects the computation layer input.
Decision matrix:
```
Tool output → valuable for LLM attention?
├── No → NoMemory=true (output preserved, skipped in computation)
└── Yes → Contains cleanable noise?
├── Yes → Cleaner filters before computation
└── No → Normal memory, no extra handling
```
> **Note**: `Cleaner` is a Go `func` type (`json:"-"`), cannot cross C ABI boundaries. Not available for Lua plugins or remote plugins.
#### Stage Hooks — Intervene in message processing flow
7 stages:
| Stage | Timing | Purpose |
|-------|--------|---------|
| `on_input` | Message just arrived at Agent | Blacklist, rate-limit, short-circuit |
| `pre_action` | About to call LLM | Inject context |
| `post_action` | LLM returned results | Modify output/tool list |
| `before_toolcall` | Before tool execution | Audit, reject, modify params |
| `after_toolcall` | After tool execution | Desensitize, rewrite results |
| `before_output` | Before output | Format adaptation, leak cleanup |
| `after_output` | After output | Statistics/logging |
```go
// Global: receive all stage events
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
ctx.Lock()
ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{
"role": "system",
"content": "Injected context content",
})
ctx.Unlock()
return nil
})
// Own tools only: only before_toolcall/after_toolcall for this plugin's tools
s.RegisterStage(sdk.StageBeforeToolcall, myHandler, sdk.StageScopeOwnTools)
```
#### Configuration Management
```go
// Register config definition
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "plugin.myplugin.api_key",
Default: "",
Type: "string",
DisplayName: "API Key",
Description: "API key",
Category: "myplugin",
})
// Read/write config
val, err := s.Settings().Get("api_key")
s.Settings().Set("api_key", "new-value")
// Read core config
s.Settings().GetCore("llm.model")
// Read other plugin's config
s.Settings().GetPlugin("other_plugin", "some_key")
```
#### Input Delivery
```go
// Normal delivery (processed in order)
s.InjectText(source, channel, text string)
// Interrupt delivery (can interrupt current LLM processing)
s.InjectInterruptText(source, channel, text string)
// No memory recording
s.InjectTextNoMemory(source, channel, text string)
```
#### Event Subscription
```go
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
unsub := s.Events().Subscribe(sdk.EventToolCall, func(evt *sdk.Event) {
log.Printf("Tool was called: %v", evt.Payload)
})
defer unsub()
```
#### Capability Access
```go
// Graph Memory (entity-relation store)
entities, relations, err := s.Memory().Recall([]string{"keyword"}, 2)
// Document Memory (vector store)
docs := s.DocMemory().Query("query text", 3)
// Knowledge
results, err := s.Knowledge().Search("query", 5)
// LLM source management
s.LLM().ListSources() // returns []string
s.LLM().SetSource("deepseek")
```
#### Event Subscription (built-in plugins)
```go
// Subscribe to system events, returns unsubscribe function
unsub := s.Subscribe("tool_call", func(evt *events.Event) {
log.Printf("Tool was called: %v", evt.Payload)
})
defer unsub()
// Publish event
s.Publish(&events.Event{
Type: "custom_event",
Payload: map[string]interface{}{"key": "value"},
})
```
#### IO Channel Management (built-in plugins)
```go
// Register a channel (bind device driver), dev must implement the agentIO.Device interface:
// Name() string
// Type() DeviceType
// Description() string
// Tools() []ToolDef
// Execute(tool string, args map[string]interface{}) (interface{}, error)
// Start() error
// Stop() error
// OutputCapabilities() OutputCapability
s.RegisterChannel("mydevice", deviceImpl)
// Unregister a channel
s.UnregisterChannel("mydevice")
// List all channels
channels := s.ListChannels()
```
#### Input Delivery (built-in plugins)
```go
// Queued delivery (processed in order)
s.InjectInput(source, channel, eventType string, payload map[string]interface{})
// Synchronous delivery (waits for response)
resp := s.InjectInputSync(source, channel, eventType string, payload map[string]interface{})
// Interrupt delivery (can preempt current LLM processing)
s.InjectInterrupt(source, channel, eventType string, payload map[string]interface{})
// Synchronous text shortcuts
resp := s.InjectTextSync(source, channel, text string)
resp := s.InjectTextSyncNoMemory(source, channel, text string)
// Get output channel
outputCh := s.OutputChan()
```
> **Note**: `Subscribe`, `Publish`, `RegisterChannel`, `UnregisterChannel`, `ListChannels`, `InjectInput`, `InjectInputSync`, `InjectInterrupt`, `InjectTextSync`, `InjectTextSyncNoMemory`, `OutputChan` are only available in built-in plugins (`internal/sdk` package). External dynamic plugins should use the public APIs: `InjectText`, `InjectInterruptText`, `InjectTextNoMemory`.
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 3. Lua Plugin Development in Detail
Lua plugins are suitable for lightweight rapid prototyping, requiring no Go compilation environment. Changes take effect after kernel restart.
### Plugin Structure
```lua
-- main.lua
local plugin = {
name = "myluaplugin"
}
function plugin.start(sdk)
sdk.log("info", "myluaplugin starting...")
sdk.register_tool("myluaplugin_hello", {
description = "A hello world tool",
parameters = {
type = "object",
properties = {}
}
}, function(args)
return { content = "Hello from myluaplugin plugin!" }
end)
sdk.log("info", "myluaplugin started")
end
function plugin.stop()
sdk.log("info", "myluaplugin stopped")
end
return plugin
```
### SDK Mock Layer
`sdk.lua` provides a pure Lua SDK mock implementation, supporting `lua main.lua` standalone testing:
```bash
lua main.lua
# Output:
# [lua-plugin] info: myluaplugin starting...
# [lua-plugin] register_tool: myluaplugin_hello
# [lua-plugin] info: myluaplugin started
```
When running inside the kernel, `sdk.*` global variables are injected by the Go layer, and all functions marked with `-- !impl` are replaced with real implementations.
### Lua SDK API
| Function | Description |
|----------|-------------|
| `sdk.log(level, msg)` | Log output |
| `sdk.register_tool(name, def, handler)` | Register tool |
| `sdk.register_stage(stage, handler)` | Register stage hook |
| `sdk.register_api(name)` | Register API |
| `sdk.get_setting(key)` | Read config |
| `sdk.set_setting(key, value)` | Write config |
| `sdk.inject_text(source, channel, text)` | Deliver text message |
| `sdk.inject_interrupt(source, channel, text)` | Interrupt delivery |
| `sdk.json.encode(val)` | JSON encode |
| `sdk.json.decode(str)` | JSON decode |
| `sdk.http.get(url)` | HTTP GET request (`-- !impl`) |
| `sdk.http.post(url, body, content_type)` | HTTP POST request (`-- !impl`) |
> **Note**: Lua plugin's `sdk.register_stage` callback currently only receives `raw_message`, `user_id`, `phase` fields. The functionality is limited. For complex stage handling logic, use Go plugins.
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 4. Built-in Plugins
Built-in plugins use `init()` self-registration, compiled into the kernel, no separate deployment needed.
### Directory Structure
```
internal/plugins/yourplugin/
plugin.go — Plugin main file
```
### Minimal Plugin Example
```go
package yourplugin
import (
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func init() {
plugin.RegisterFactory("yourplugin", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
})
}
type Plugin struct {
name string
}
func New(name string) *Plugin {
return &Plugin{name: name}
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
// Initialize plugin here: start goroutines, register tools, subscribe events, etc.
return nil
}
func (p *Plugin) Stop() error {
// Clean up resources
return nil
}
```
### Register with Kernel
Add blank import in `internal/plugins/all.go`:
```go
package plugins
import (
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/yourplugin"
// ... other plugins
)
```
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 5. Best Practices
1. `Start()` is non-blocking — start long tasks in goroutines, don't block Start
2. `Stop()` cleans up resources — close connections, stop goroutines, cancel subscriptions
3. Unique tool names — use plugin name prefix to avoid conflicts
4. When handler returns `error`, LLM will receive it and may retry
5. Use `InjectInterruptText` for interrupts, `InjectText` for normal delivery
6. Use `Settings().Get/Set` for config, don't hardcode
7. External Go plugins compile independently, not tied to kernel version; only built-in plugins need recompilation with kernel
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 6. Example Plugin Reference
### SDK Repository Examples (`homeagent-sdk/example/`)
| Example | Type | Features |
|---------|------|----------|
| [memo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/memo) | Go | Memo management, PreAction injection + timed interrupt dual reminder |
| [files](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/files) | Go | File system operations, 4 write modes, sandbox isolation |
| [browser](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/browser) | Go | Web search + HTTP fetch (SSRF) + Chromium render (merged from web/webfetch) |
| [bili](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/bili) | Go | Bilibili video download (yt-dlp) |
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot integration, 17 tools |
| [editdoc](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/editdoc) | Go | Office document editing and format conversion |
| [a2a](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/a2a) | Go | Agent-to-Agent protocol |
| [ocr](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ocr) | Go | Offline text recognition (Tesseract) |
| [sanitizer](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/sanitizer) | Go | Output sanitizer filter |
### Built-in Plugins
| Plugin | Location | Features |
|--------|----------|----------|
| Timer | `internal/plugins/timer/` | Simplest complete example, registers one tool + interrupt feedback |
| CLI | `internal/plugins/cli/` | Unix socket listener + synchronous request-response |
| WebUI | `internal/plugins/webui/` | HTTP service + dependency injection |
---
*Want to understand the project goals? See [OVERVIEW.md](OVERVIEW.md).*
*Want to understand the architecture? See [ARCHITECTURE.md](ARCHITECTURE.md).*
*SDK repository and development tools? See [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk).*

120
assets/docs/zh/ADAPTER.md Normal file
View File

@ -0,0 +1,120 @@
[English](../en/ADAPTER.md) | **中文**
# Lua Adapter — LLM 源适配指南
每个 LLM API 源对应一个 Lua 脚本负责请求转换Go 统一格式 → API 格式和响应转换API 格式 → Go 统一格式)。
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 适配器契约
Lua 脚本必须返回一个包含以下字段和函数的 table
```lua
local adapter = {}
-- 元信息
adapter.name = "my_provider" -- 唯一标识,与 config 中 adapter 字段一致
adapter.version = "2.0.0"
adapter.endpoint = "/v1/chat/completions" -- API 路径,拼接到 base_url 后
adapter.headers = {} -- 额外 HTTP 请求头
-- 请求转换Go → API
function adapter.transform_request(raw_json)
-- raw_json: Go 的 CompletionRequest JSON 字符串
-- 返回: 应发送给 API 的 JSON 字符串
return transformed_json
end
-- 响应转换API → Go
function adapter.transform_response(raw_json)
-- raw_json: API 返回的原始 JSON 字符串
-- 返回: 统一 CompletionResponse JSON 字符串
-- 统一格式:
-- { content: "", finish_reason: "", token_usage: { prompt: N, completion: N, total: N }, tool_calls?: [...] }
return unified_json
end
-- 流式块转换(可选)
function adapter.transform_stream_chunk(raw_line)
-- raw_line: SSE 中 data: 后的 JSON 字符串
-- 返回: { content: "", done: bool } 的 JSON返回 "" 表示跳过该 chunk
return chunk_json
end
return adapter
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 统一 CompletionRequest 格式Go → Adapter
```json
{
"model": "deepseek-v4-flash",
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "...", "tool_calls": [...] }
],
"temperature": 0.7,
"max_tokens": 4096,
"stream": false,
"tools": [...],
"tool_choice": "auto"
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 统一 CompletionResponse 格式Adapter → Go
```json
{
"content": "回复内容",
"finish_reason": "stop",
"token_usage": { "prompt": 10, "completion": 20, "total": 30 },
"tool_calls": [
{ "id": "call_xxx", "type": "function", "name": "tool_name", "arguments": { "key": "val" } }
]
}
```
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## Lua VM 内置函数
`json.encode(table)` — 将 Lua table 编码为 JSON 字符串
`json.decode(string)` — 将 JSON 字符串解码为 Lua table
`log(level, message)` — 输出日志level: info/warn/error
`http_get(url)` — 发起 HTTP GET 请求,返回响应体字符串
`http_post(url, body)` — 发起 HTTP POST 请求,返回响应体字符串
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 适配典型 API
| API | endpoint | auth 方式 | 格式差异 |
|---|---|---|---|
| **OpenAI** | `/chat/completions` | `Authorization: Bearer <key>` | 标准 OpenAI 格式 |
| **DeepSeek** | `/chat/completions` | `Authorization: Bearer <key>` | OpenAI 兼容,强制 temperature=0 |
| **Anthropic** | `/v1/messages` | `x-api-key: <key>` | Messages APIsystem 消息分离content 为 block 数组 |
| **Gemini** | `/v1/models/{model}:generateContent` | `?key=<key>` 或 Bearer | contents/parts 格式role 用 model 而非 assistant |
| **Mistral** | `/v1/chat/completions` | `Authorization: Bearer <key>` | OpenAI 兼容 |
| **Groq** | `/openai/v1/chat/completions` | `Authorization: Bearer <key>` | OpenAI 兼容 |
| **GitHub Models** | `/chat/completions` | `Authorization: Bearer <pat>` | OpenAI 兼容 |
| **Ollama** | `/api/chat` | 无 | 不同的 options 格式 |
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 添加新源步骤
1.`internal/lua/adapters/` 下创建 `<name>.lua`
2. 脚本定义 `transform_request``transform_response`
3. (可选)定义 `transform_stream_chunk` 支持流式
4. 编译验证:`go build ./cmd/homed/`
5. 测试验证:`go test ./...`

View File

@ -0,0 +1,441 @@
[English](../en/ARCHITECTURE.md) | **中文**
# HomeAgent 架构
## 体系结构原则
HomeAgent 的认知架构由三个核心子系统构成事件循环eventLoop、上下文窗口RelevanceContext与阶段管道StageHost。三者共同组成编排框架。LLM 在其中作为可调度的推理执行单元运行;认知连续性由事件循环、上下文窗口与阶段管道维持。
**事件循环eventLoop** 是一个三路 select 循环:`a.io.InputChan()` 接收外部用户输入并分发至 `processTextInput` / `processMediaInput``a.selfInputCh` 接收内部系统任务(如记忆合并、蒸馏回调),以 `_consolidation_` 输出通道标识区分,走 `processConsolidation` 路径;`a.ctx.Done()` 接受关闭信号。与之并行运行的 `interceptLoop` 协程独立监听 `a.io.InputInterruptChan()`,收到高优先级中断时先取消当前 LLM HTTP 请求(`a.cancelLLM()`),再将事件写入 `a.interceptCh`——该通道在 `process()` 每次 LLM 调用前由 `drainInterrupts()` 非阻塞排空,以 `[打断消息]` 格式注入消息历史。三条中断投递路径各具语义:`cancelLLM` 终结当前 HTTP 请求,`interceptCh` 在下一轮 LLM 调用前注入文本,`InjectInput` 在 eventLoop 空闲时触发新一轮处理。
**阶段管道StageHost** 管理两类注册工具定义ToolDef与阶段处理器StageHandler。ToolDef 包含 `NoMemory bool``Cleaner func(string) string` 两个可选的记忆控制字段:`NoMemory=true` 时工具输出不参与向量化/jieba/蒸馏计算(原文保留);`Cleaner` 在输出进入计算层前执行过滤(如提取 JSON 的 `content` 字段)。两者均不修改原文,只影响计算层输入。`RegisterTool` 拒绝同名注册,推断工具所属插件名,并维护工具到插件的映射表 `toolPlugins``RegisterStage` 将处理器追加至对应阶段的处理器列表。触发阶段执行时(`RunStage`**所有已注册处理器通过 goroutine 并行执行**,共享同一 `*StageContext` 实例(通过 `sync.RWMutex` 保护并发访问)。单个处理器的 panic 被独立恢复,不影响其他处理器。短路语义通过检查 `ctx.Response != nil` 实现——任一阶段处理器可设置此值提前终止当前链路。工具执行 `ExecuteTool` 内置 panic 恢复与栈追踪记录。`UnregisterPluginTools` 在插件热重载时移除对应工具集。
**上下文窗口RelevanceContext** 维护一个按时间排序的事件列表。`Append` 在录入前经 `CleanTemplateText` 剥离 QQ 模板与时间戳噪声再通过三分支向量策略agent 事件用 Response用户事件用 Inputcold_storage 用 Input+Response计算嵌入向量。`Prune` 在事件数超过 `topK` 时触发,**无条件保护最近 10 条事件不被裁剪**recency bias对剩余候选事件计算与当前输入的 CosineSimilarity按评分降序保留 `topK - 10` 条(下限为 0之后按时间戳重排序。裁剪出的事件中过滤掉 `agentcli``terminal` 来源后,其余通过 `docStore.ContextToDoc` 归档至 Document 层,保留原始时间戳。持久化采用 5 秒防抖写入磁盘 JSON 文件。
**工具定义聚合自五个来源**IOManager 注册的插件工具StageHost 注册的 SDK 插件工具Indexer 提供的记忆索引工具;内置条件工具(依据 memory / knowledge / docStore / social / pluginReg / providerManager 等模块的非空状态选择性添加包括记忆操作、知识检索、文档查询、社交网络、插件重载、子代理生成、输出通道工具、LLM 源切换等);以及按 `pendingMedia` 状态添加的媒体处理工具。`buildToolDefs()` 在每次 process 周期中重新聚合所有这些来源。
**Provider 调用采用有序降级策略**`ProviderManager.OrderedProviders()` 返回按注册顺序排列的 provider 列表。`process()` 内循环遍历该列表依次尝试 `Chat()` 调用。401/403 状态码将对应 provider 标记为永久不可用;其他错误类型同样标记不可用但容忍度更高。全部 provider 失败时返回错误返回调用方。若调用因 context 取消而中断且 Agent 仍在运行,则重试(仅当处理非 consolidation 路径时)。
**记忆体系采用三级存储层级结构Context → Document → Graph按访问局部性与持久化需求进行数据分置**Context 层为高速易失工作窗口,使用 StaticEmbedder预训练词嵌入进行语义相关性评分以 TF-IDF 为回退策略Document 层与 Context **共享同一 StaticEmbedder 向量空间**agent 启动时将 embedder 注入 Document Store使 Context 裁剪时的事件相关性评分与 Document 查询时的语义检索处于同一向量空间中确保冷热数据之间的相似度可比——TF-IDF 仅在 embedder 未加载时作为兜底方案Graph 层以 SQLite 为持久化载体entities 表与 relations 表分别存储节点与有向边,支持 BFS 遍历召回。三级之间定义数据迁移策略:低分事件从 Context 下沉至 Document以归档时相同的 embedder 向量化写入),冷文档经 72 小时未访问阈值判定后通过 `docToTriples` 蒸馏为三元组写入 Graph。Indexer 通过双路召回(实体向量相似度搜索 + jieba 关键词提取)构建 Graph 查询种子,结合 `MarkRecalled` 机制避免已被工具调用取回的实体重复注入系统提示。
**核心域与应用域的职责域分离**是一项系统级架构决策:内核的责任边界限定在 LLM 编排、记忆管理与知识检索三个维度内,不直接承载任何 IO 操作;所有外部交互通过插件域接入。该分离将核心域的复杂度控制在可验证范围内,同时赋予插件域独立的演化自由度——后者可独立开发、独立发布、热加载,且不对核心域的稳定性构成直接影响。
## 消息处理流程
### 完整链路
```
外部输入(通过插件 InjectInput
eventLoop() → processTextInput()
├── on_input stage 插件可拦截/改写/短路
├── Context.Append 记录到上下文窗口
├── Context.Prune 低相关性事件归档到 Document
├── buildMemoryContext() Indexer 召回 → GraphDB BFS 遍历
├── pre_action stage 插件可注入 system 消息
├── [工具循环] process()
│ ├── buildSystemPrompt 人格 + 记忆 + 知识 + 上下文
│ ├── buildToolDefs 内置工具 + 插件工具
│ ├── provider.Chat() LLM 调用
│ ├── post_action stage 插件可见 LLM 输出 + 工具列表
│ ├── 有工具?
│ │ ├── before_toolcall 插件可拒绝/改参
│ │ ├── executeToolCall 路由到插件/内置
│ │ ├── after_toolcall 插件可改结果
│ │ └── → 回到 post_action
│ └── 无工具 → 退出循环
├── Context.Append(response)
├── before_output stage 插件可改最终文本
├── emitResponse() 通过 output_send 发送
└── after_output stage 插件只读,收尾
```
代码:`internal/agent/core/process.go``process()` 是工具循环主体
### 7 个阶段钩子
| 阶段 | 触发时机 | 插件可做 |
|------|----------|----------|
| `on_input` | 消息到 Agent零处理 | 黑名单/限流/短路回复 |
| `pre_action` | 上下文就绪LLM 调用前 | 注入外部数据到 context |
| `post_action` | LLM 返回文本+工具列表 | 敏感词过滤/强制 redirect |
| `before_toolcall` | 单个工具执行前 | 审计/拒绝/改参 |
| `after_toolcall` | 单个工具执行完 | 脱敏/排序结果 |
| `before_output` | 最终文本就绪,发送前 | 格式适配 |
| `after_output` | 已发送 | 统计日志 |
代码:`internal/agent/core/stages.go``StageHost` 编排
### 循环规则
`post_action → [before_toolcall → 执行 → after_toolcall] → post_action` 构成内循环。
退出条件LLM 无工具调用 / 全部被拒绝 / 超上限。
### 短路规则
任意阶段设 `ctx.Response` 即跳到 `after_output`
## 三层记忆
### 记忆流转
```
① Context (工作窗口)
RelevanceContext — 内存 events[] + JSON持久化
Append: 每次输入, CleanTemplateText → 三分支向量(textForVector)
agent事件→Response, 用户事件→Input, cold_storage→Input+Response
StaticEmbedder 预训练词嵌入 / TF-IDF 回退
Prune: StaticEmbedder CosineSimilarity, 保留 topK + 最近10条
├── 保留 → timeline → 按时间排序 → system prompt
└── 低分 → Document 层归档 (原始时间戳)
Save: 5s debounce 写盘
↓ Prune 归档 ↑ LLM 主动召回
② Document (文件记忆)
DocStore — JSON文件 + 与 Context 共享的 StaticEmbedder 向量空间(兜底: TF-IDF InvertedIndex
写入: Prune归档 / doc_commit / Graph快照(syncGraphToDocs)
读取:
├── 自动注入: Query(input, top3) → 同一向量空间下相似度摘要 → 【相关记忆文档】→ system prompt (只读)
└── LLM主动: doc_query → Consume(读取并删除)
→ 逐条 context.Append{Timestamp: d.CreatedAt, Source: "cold_storage"}
→ 文档以原始时间戳写入 context 时间线, 从 docStore 删除
冷化: FindColdDocs(72h, ≤2次访问) → docToTriples → Graph
↓ 冷文档蒸馏 ↑ 自动召回
③ Graph (图数据库)
SQLite — entities + relations 表
写入: memory_commit / 冷文档蒸馏 / Pipeline 规则蒸馏 / memory_merge
读取:
├── 自动召回: Indexer.BuildContext(input)
│ → CleanTemplateText → 向量实体搜索 + jieba关键词 → SQLite LIKE + BFS depth=2
│ → 【记忆索引】→ system prompt
└── LLM主动: memory_recall / memory_merge / memory_purge / memory_edit / memory_delete_entity
Social: person_query / set_trait / relate (包装 GraphDB)
④ 蒸馏管道 (每30min心跳)
distillContext → 窗口>2×maxSize → 强制Prune
syncGraphToDocs → Graph 快照写入 Document(跨层可搜索)
reorgGraph:
Step1: indexer.Sync — 重建实体向量索引
Step2: docStore.Reindex — 重建文档向量索引
Step3: 冷文档 → docToTriples → GraphDB.Commit
Step4: 实体相似度(Bigram Jaccard>0.75) → consolidation → LLM判断合并
Step5: evaluateGraphQuality → LLM判断保留/删除
⑤ Pipeline 规则蒸馏器 (每心跳)
distillOnce → 正则匹配个人信息:
我叫X / 我住在X / 我喜欢X / 我X岁 / 我的工作是X
→ 三元组 → GraphDB.Commit
```
### 向量化:预训练词嵌入 + TF-IDF 回退
所有向量化统一使用 `StaticEmbedder``internal/memory/static_embedder.go`
**主策略 — 预训练词嵌入(词对齐 300 维)**
- 模型来源ConceptNet Numberbatch77 语对齐)/ fastText 中文 / fastText 英文
- 通过 `core.agent.embedding_model_path` 配置(逗号分隔多模型)
- 路径名含 `numberbatch` → 自动下载 ConceptNet`cc.zh.` → fastText 中文,含 `cc.en.` → fastText 英文
- 不匹配则默认 ConceptNet
- **前处理**`CleanTemplateText` 剥离 QQ 工具调用模版、时间戳噪声,避免垃圾干扰相似度
- **三分支向量来源**agent→Response用户→Inputcold_storage→Input+Response
- **TF-IDF 回退**:模型下载失败或未配置时自动回退词袋 TF-IDF服务不中断
| 位置 | 文件 | 用途 | 算法 |
|------|------|------|------|
| Context Prune | `context.go:155` | 裁剪低相关性上下文事件 | VectorizeClean → CosineSimilarity(queryVec, evt.Vector) |
| DocStore Query | `document.go:206` | 文档记忆召回 | StaticEmbedder.Vectorize首选/ TF-IDF兜底→ vec.Search |
| Indexer 实体搜索 | `indexer.go:96+111` | Graph实体召回 | 向量实体搜索 + jieba关键词 → SQLite LIKE + BFS |
| 实体相似度检测 | `distill.go` | Graph中相似实体 | Bigram Jaccard (>0.75 → consolidation) |
### Context 层
`internal/agent/core/context.go``RelevanceContext`
- 维护最近事件列表,每次 Append/Prune 写入 JSON 防丢
- 向量化前统一经 `CleanTemplateText` 去模版噪声
- 三分支 `textForVector`agent 事件用 Response、用户事件用 Input、cold_storage 用 Input+Response
- 预训练词嵌入 `StaticEmbedder` → CosineSimilarity模型不可用时自动回退 TF-IDF
- 保护最近 10 条记录免于淘汰,超出部分按相关性排序归档到文档记忆
- 归档事件以原始时间戳写入文档记忆,后续 `doc_query` 召回时按原始时间戳插回时序
### Document 层
`internal/memory/document/document.go``Store`
- 消费即删模式:`doc_query` 检索到后删除
- 双路召回:与 Context 共享的 StaticEmbedder 语义向量搜索 + jieba 关键词提取(模型未加载时回退 char-bigram TF-IDF
### Graph 层
`internal/memory/graph.go``GraphDB`
- SQLite WAL 模式两张表驱动mattn/go-sqlite3CGo
- `Commit(triples)` — UPSERT entities + INSERT relations
- `Recall(keywords, depth)` — 关键词 LIKE 搜索 + BFS 遍历
### 记忆工具LLM 可直接调用)
| 工具 | 作用 |
|------|------|
| `memory_recall` | 从 Graph 召回 |
| `memory_commit` | 写入 Graph 三元组 |
| `memory_merge` | 合并两个实体节点 |
| `memory_purge` | 删除指定实体 |
| `memory_edit` | 编辑已有实体/关系 |
| `memory_delete_entity` | 删除实体及其所有关系 |
| `memory_introspect` | 查看记忆统计 |
| `doc_query` | 从 Document 搜索 |
| `doc_commit` | 写入 Document |
### 其他记忆层
- **Social** (`internal/memory/social/social.go`) — 人格特质和关系网,包装 GraphDB 实体类型
- **Text Memory** (`internal/memory/text/text.go`) — 原始对话 JSONL 日志,轮转策略
- **Memory Indexer** (`internal/memory/indexer.go`) — 实体向量化 + jieba 关键词提取,自动注入 system prompt
### 蒸馏管道
`internal/memory/pipeline/pipeline.go`
- 10 分钟 tick7 天保留
- 规则提取三元组name / location / likes / age / job 模式)
- 写入 GraphDB
### 上下文剪枝
```
心跳 30min:
├── distillContext() — 蒸馏当前上下文
├── syncGraphToDocs() — Graph→Document 同步
└── reorgGraph()
├── Indexer.Sync()
├── DocStore.Reindex()
├── 冷文档→Graph
└── 实体冲突 → enqueueConsolidationTask()
selfInputCh → LLM 判断合并/跳过
```
实体冲突检测启发式bigram Jaccard > 0.75),走 `selfInputCh` 内部通道LLM 最终判断是否合并。
## 知识库
`internal/knowledge/knowledge.go`
- 文件目录 `knowledge/<name>/content.md`
- 独立 TF-IDF 索引,与记忆系统不冲突
- `knowledge_search` / `knowledge_create` / `knowledge_list`
## Provider 与 Lua 适配层
```
Agent
Provider 接口 (Name / Chat / ChatStream)
├── OpenAIProvider — 标准 OpenAI API
├── OllamaProvider — 本地 Ollama
└── LuaAdaptedProvider (主要)
├── 序列化 CompletionRequest → JSON
├── adapter.transform_request() → API 格式
├── HTTP 请求 + adapter.headers
├── adapter.transform_response() → 统一格式
└── 反序列化
```
代码:`internal/agent/api/provider.go`
ProviderManager 管理多个源,按注册顺序 fallback。Lua 适配器位于 `internal/lua/adapters/`,每个 `.lua` 脚本定义 `transform_request` / `transform_response` / `transform_stream_chunk`
VM 内置 `json.encode` / `json.decode` / `log` / `http_get` / `http_post`
## 插件系统
### 四种加载方式
| 方式 | 注册机制 | 编译 | 用途 |
|------|----------|------|------|
| 内置插件 | `init()``RegisterFactory` | `internal/plugins/` 编译进内核 | webui/cli/timer/mcp 等 |
| 外部 `.so` | C ABI 动态加载 | `-buildmode=c-shared` + bridge | qq/files/web/memo 等 |
| Lua 脚本插件 | 解析 `main.lua` 注册工具 | 无需编译,热加载 | luaplugintest/testlua 等 |
| SKILL 插件 | 解析 `SKILL.md` | Markdown 定义 | clawhubadapter 兼容加载 |
内置插件注册:`internal/plugins/all.go` 空白导入 → 各插件 `init()``Registry.Load()` 扫描目录匹配工厂。
外部插件加载:`internal/plugin/dynamic.go` → 复制到 SHA256 临时路径(绕过 `plugin.Open` 路径缓存)→ `Open` + `Lookup("NewPlugin")`
Lua 脚本插件加载:`internal/lua/` → 通过 Lua VM 解析 `main.lua`,调用 `start()` 注册工具。
### PluginSDK 四通道
```
插件 ──→ 核心
RegisterTool(name, fn) ──→ buildToolDefs() / executeToolCall()
RegisterStage(stage, fn, scope...) ──→ runStage() 在对应阶段调用scope 控制全局/仅自己工具)
Subscribe(event, fn) ──→ Publish() 通知所有订阅者
RegisterOutputChannel(name, caps, desc, handler) ──→ output_send__{name} 工具生成
```
`internal/sdk/` 桥接外部 SDK 接口到内核,定义完整 PluginSDK
```go
sdk.RegisterTool(name, def, handler)
sdk.RegisterStage(stage, handler, scope...)
sdk.Publish(event)
sdk.InjectInput(source, channel, payload)
sdk.InjectInterrupt(source, channel, payload)
sdk.Memory().Recall/Commit
sdk.Knowledge().Search/Create
sdk.Settings().Get/Set/List
sdk.RegisterOutputChannel("qq", sdk.CapText|sdk.CapAudio|sdk.CapImage, "QQ消息通道详见 output_send__qq_help", handler)
```
### Plugin 接口
```go
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
```
## 输出通道系统
每个输出通道生成两个工具:
| 工具 | 类型 | 作用 |
|------|------|------|
| `output_send__{name}` | function | 接受 `payload`(消息载荷)、`meta`(JSON 路由元数据)、`type`(枚举) 三个参,路由到插件 handler |
| `output_send__{name}_help` | function | 返回通道的 meta 格式和 type 枚举说明 |
能力标志位:
| 标志 | 值 | 含义 |
|------|-----|------|
| CapText | 1 | 纯文本 |
| CapFile | 2 | 文件 |
| CapImage | 4 | 图片 |
| CapAudio | 8 | 音频 |
| CapStructured | 16 | 结构化数据 |
系统提示注入:输出门控规则、多调用支持、长消息拆分。
子代理权限:`output_send__` 前缀工具允许使用。
## LLM 链事件
- 事件类型 `agent_llm_chain`,每次 LLM 轮次后发射
- 包含完整 LLM 响应(文本 + 工具调用 + 推理)
- WebUI 通过 SSE 订阅此事件实现实时显示
- 插件可通过 EventSubscriber 订阅(外部插件只读)
## 受限外部插件 API
分层架构:内部插件获得完整 PluginSDK外部插件获得受限 SDK。
| API | 内部插件 | 外部插件 |
|-----|----------|----------|
| SocialAPI | 完整读写 | 只读GetPerson / GetTrait / GetRelations / GetNetwork / ListPersons |
| EventSubscriber | 订阅 + 发布 | 仅订阅(无 Publish 能力) |
扩展字段:
- Triple 扩展Confidence、SubjectType、ObjectType
- Relation 扩展Confidence
## 中断机制
```
interceptLoop (goroutine)
├── InputInterruptChan() ← 定时器/消息通知
├── (a) cancelLLM() → 取消 Provider HTTP 请求
├── (b) interceptCh → process() 轮前读 [打断消息]
└── (c) InjectInput() → 空闲时触发新处理
```
三种投递路径:
| 路径 | 效果 | 时机 |
|------|------|------|
| cancelLLM | 取消当前 HTTP 请求 | 收到 context.Canceled |
| interceptCh | process() 中插入 `[打断消息]` | 每个 LLM call 前 |
| InjectInput | eventLoop 空闲时触发新处理 | 无进行中请求 |
代码:`internal/agent/core/eventloop.go``interceptLoop` / `drainInterrupts`
## 配置系统
`internal/config/registry.go` — ConfigRegistry
- SQLite 存储,`config` 表 + `config_<plugin>` 独立表
- 命名空间:`core.*` / `plugin.<name>.*`
- `RegisterDefault` 插入 ~80 个默认键8 个 LLM 源的 seeds
- WebUI 设置页 `/api/v1/settings` 读写
## 代码结构
```
cmd/homed/main.go — 入口:组装所有子系统
cmd/waiter/main.go — CLI 客户端 (Unix socket)
internal/
├── agent/
│ ├── core/ — Agent 核心 (eventLoop/process/stages/context)
│ │ └── plugin_health.go — 插件健康监控与自动重启
│ ├── api/ — Provider 接口 + LuaAdaptedProvider
│ ├── io/ — IOManager (排队/中断/输出)
│ └── personal.go — 人格加载
├── plugin/
│ ├── registry.go — 注册表 + 生命周期
│ ├── dynamic.go — .so 动态加载器
│ └── manifest.go — plugin.json 元数据
├── plugins/ — 内置插件实现
│ ├── all.go — 空白导入
│ ├── webui/ — HTTP 服务器 + 嵌入式 SPA
│ ├── cli/ — Unix socket CLI
│ ├── timer/ — 定时器
│ ├── cmd/ — 命令执行
│ ├── mcp/ — MCP 协议
│ ├── files/ — 文件操作
│ ├── clawhubadapter/ — ClawHub 适配器OC 插件/SKILL/JS/Python sidecar
│ ├── agentcli/ — PTY 终端
│ ├── healthcheck/ — 健康检查
│ ├── pluginmgr/ — 插件管理器
│ └── cfgmgr/ — 配置管理
├── sdk/ — PluginSDK 定义
│ ├── plugin.go — Plugin 接口 + PluginSDK
│ ├── memory.go — MemoryAPI
│ ├── knowledge.go — KnowledgeAPI
│ ├── settings.go — SettingsAPI
│ └── llm.go — LLMAPI
├── memory/
│ ├── graph.go — SQLite 图数据库
│ ├── indexer.go — 图→向量索引
│ ├── vector/store.go — TF-IDF 向量引擎
│ ├── document/document.go — 文档记忆
│ ├── text/text.go — 文本日志
│ └── pipeline/ — 蒸馏器
├── knowledge/knowledge.go — 知识库
├── lua/
│ ├── vm.go — Lua VM (json/log/http)
│ └── adapters/ — 8 个 LLM 适配器脚本
├── config/registry.go — SQLite 配置中心
├── events/bus.go — 事件总线
├── tracker/ — OverlayFS 变更追踪
├── supervisor/ — 守护进程管理
├── skill/ — Skill 插件管理
│ └── manager.go — Skill 加载/匹配
└── meta/ — 元信息
└── meta.go — Agent 元数据
```

View File

@ -0,0 +1,83 @@
[English](../en/OVERVIEW.md) | **中文**
# HomeAgent — 项目概览
## 这是什么
HomeAgent 是一个持续运行的个人智能 Agent 框架。
核心架构:一个长时间运行的内核进程(`homed`),通过插件系统接入各种 IO 通道QQ、Web、命令行等。内核负责 LLM 调用编排、记忆管理、知识检索;插件负责所有外部 IO——收发消息、执行文件操作、搜索网络等。
### 设计要点
**核心域与应用域分离** — 内核(核心域)不执行任何 IO 操作,所有 IO 能力归属插件(应用域)。边界通过 PluginSDK 明确定义:
- 插件向内核注册工具Tool供 LLM 调用
- 插件挂入处理管道Stage在各阶段拦截/改写消息流
- 插件订阅/发布事件Event松耦合通信
- 插件通过 IO API 排队或打断投递输入
这一划分的意义在于职责边界清晰:内核专注于编排与记忆管理,插件负责具体 IO 实现,二者互不耦合。
**三层记忆架构** — 通过分级存储策略管理 Agent 长周期运行中的信息留存:
- **Context 层**内存中局部词嵌入评分的事件窗口jieba + TF-IDF + PMI → CosineSimilarity实时维护最近上下文低相关性事件自动下沉到下一层
- **Document 层**JSON 文件 + TF-IDF 向量索引的临时记忆,支持显式提交和隐式归档,冷数据蒸馏到 Graph
- **Graph 层**SQLite 图数据库持久化实体entities和关系relationsBFS 遍历召回,蒸馏管道从对话中提取三元组
三层递进:上下文 → 冷归档 → 长期图记忆,构成从短期到持久的信息衰减与整合管道。
## 它实际做了什么
代码位于项目仓库根目录Go 语言实现。
**内核** (`internal/agent/core/`)
- `eventloop.go` — 消息循环(`eventLoop`),从 IO 层排队接收输入
- `process.go` / `stages.go` — 处理管道:记忆召回 → 人格注入 → LLM 调用 → 工具执行 → 输出发送7 阶段钩子
- `toolcall.go` — 工具调度与执行
- `context.go` — 上下文管理(预训练词嵌入评分 StaticEmbedder → CosineSimilarityTF-IDF 回退),自动剪枝低相关性事件
- LLM 调用通过 Provider 接口抽象,支持 8 个 LLM 源自动降级
**记忆系统** (`internal/memory/`)
- **GraphDB** (`graph.go`) — SQLiteentities + relations 表BFS 遍历
- **Document Store** (`document/document.go`) — 临时记忆JSON 文件 + TF-IDF 向量索引,消费即删
- **Text Memory** (`text/text.go`) — 原始对话日志JSONL 文件轮转
- **Social Store** (`social/social.go`) — 人格特质 + 关系网,包装 GraphDB
- **Memory Indexer** (`indexer.go`) — 自动将 GraphDB 实体向量化,用户输入时召回注入 system prompt
**知识库** (`internal/knowledge/knowledge.go`)
- 文件系统目录 `knowledge/<name>/content.md`
- TF-IDF 向量搜索,独立于记忆系统的索引实例
- LLM 通过 `knowledge_search` / `knowledge_create` / `knowledge_list` 三个工具操作
**插件系统** (`internal/plugin/`)
- 内置插件Go `init()` 自注册,编译进内核
- 外部插件Go `-buildmode=c-shared` 编译为 `.so`,通过 C ABI bridge 动态加载;也支持 Lua 脚本插件
- PluginSDK (`internal/sdk/`) 定义四通道RegisterTool / RegisterStage / Subscribe / RegisterOutputChannel
- 阶段钩子 7 个on_input → pre_action → post_action → before_toolcall → after_toolcall → before_output → after_output
**LLM Provider** (`internal/agent/api/provider.go`)
- Provider 接口Name / Chat / ChatStream
- 三种实现OpenAIProvider标准 OpenAI API、OllamaProvider本地、LuaAdaptedProviderLua 胶水适配)
- LuaAdapter 位于 `internal/lua/adapters/`,每个 LLM 源对应一个 `.lua` 脚本
- 内置 8 个适配器deepseek / openai / anthropic / gemini / mistral / groq / github / ollama
**WebUI** (`internal/plugins/webui/`)
- 嵌入式 SPA 仪表盘(`dashboard.html` 通过 `//go:embed` 打包)
- REST API状态查询、配置管理、记忆操作、知识库管理、插件管理
- 兼容 OpenAI API 格式的 `/v1/chat/completions` 端点
- SSE 事件流 `/api/v1/chat/events`
**ClawHub 适配器** (`internal/plugins/clawhubadapter/`)
- 统一加载 OC 插件Node.js、Python sidecar、JS sidecar、SKILL 四种插件类型
- RegistryDispatcher 模式Tool/Provider/Channel/Stage 注册通知分发
- ClawHub 市场搜索与安装:`clawhubadapter_search` / `clawhubadapter_npm_install`
- 9 种 Provider 类型映射为 LLM 可用工具(图片生成、搜索、语音等)
- OC 通道自动注册为 IO 设备,支持文本/文件/图片/音频能力标志
## 项目状态
核心功能已可运行。插件系统和 SDK 已就绪,可独立开发外部插件。
- 内置插件webui / cli / timer / cmd / mcp / agentcli / healthcheck / pluginmgr / clawhubadapter / files / cfgmgr
- 外部插件示例([homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 仓库 `example/`,含 Go 和 Lua 两种类型qq / files / web / memo / bili / editdoc / a2a / ocr / sanitizer / luaplugintest / testlua
- 打包分发:`.hmap` 插件包格式,通过 WebUI 安装

View File

@ -0,0 +1,632 @@
[English](../en/PLUGIN_DEV.md) | **中文**
# HomeAgent 插件开发指南
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 概述
HomeAgent 的所有外部交互能力都来自插件。插件通过 `PluginSDK`Go API与内核交互。
**SDK 仓库**:插件开发工具、模板代码和示例插件统一托管在
[homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 仓库。
```bash
git clone https://gitcode.com/JianFeeeee/homeagent-sdk.git
cd homeagent-sdk
```
每个插件实现一个三方法接口:
```go
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
```
### 三种开发方式
| 方式 | 适用场景 | 复杂度 |
|------|---------|--------|
| **动态 .so/.dll 插件(推荐)** | 独立分发的第三方插件 | 中等,使用 `plugindev` 工具链生成 |
| **内置插件** | 随 HomeAgent 一起发布 | 简单,需合入主仓库 |
| **Lua 脚本插件** | 轻量快速原型 | 简单,使用 `plugindev init --lua` 生成 |
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 一、快速开始:使用 plugindev 工具链
`plugindev` 是 SDK 仓库提供的统一插件开发工具链,支持 Go 和 Lua 两种插件类型。
### 安装
```bash
cd homeagent-sdk/tools/plugindev
go build -o plugindev
# 将 plugindev 加入 PATH 或直接使用
```
### SDK 版本管理
`plugindev sdk` 子命令管理本地 SDK 版本:
```bash
plugindev sdk list # 列出已安装的 SDK 版本
plugindev sdk current # 显示当前使用的 SDK 版本
plugindev sdk latest # 显示最新可用版本
plugindev sdk install v0.7.1 # 安装指定版本
plugindev sdk use v0.7.1 # 切换使用版本
plugindev sdk path # 显示当前 SDK 路径
```
SDK 存储在 `~/.homeagent/plugindev/sdk/<version>/``plugindev init` 自动读取当前 SDK 版本填充 `go.mod`
### 创建 Go 插件
```bash
plugindev init myplugin
cd myplugin
# 编辑插件代码
vim plugin.go
# 编译打包
plugindev build
# 输出: dist/myplugin_linux_amd64.hmap (或 windows_amd64)
```
### 创建 Lua 插件
```bash
plugindev init myluaplugin --lua
cd myluaplugin
# 编辑插件代码
vim main.lua
# 本地测试
lua main.lua
# 编译打包
plugindev build
# 输出: dist/myluaplugin_lua.hmap
```
### 模板项目结构
**Go 插件**
```
myplugin/
├── plg.json — 插件元信息(名称、版本、入口、目标平台 targets
├── plugin.go — 插件实现Plugin 接口 + 导出函数 NewPlugin
├── go.mod — Go 模块定义
├── README.md — 说明文档
└── thirdpart/ — 外部源码存放目录(可选)
```
编译时自动生成 C ABI bridge 文件(`z_bridge_gen.go` + `z_entry.c`),无需手动创建。
**Lua 插件**
```
myluaplugin/
├── plg.json — 插件元信息entry: "main.lua", targets: "lua"
├── main.lua — 插件实现Plugin 接口的 Lua 版本)
├── sdk.lua — SDK 模拟层(支持 `lua main.lua` 独立测试)
└── README.md — 说明文档
```
### 编译打包
`plugindev build` 会自动完成编译和打包:
```bash
cd myplugin
plugindev build
```
执行过程:
1. 读取 `plg.json``targets` 字段确定目标平台
2. 自动生成 C ABI bridge 代码(`z_bridge_gen.go` + `z_entry.c`
3. **Go 插件**:执行 `go build -buildmode=c-shared`(生成 `.so` / `.dylib` / `.dll`
4. **Lua 插件**:直接打包源码,无需编译
5. 生成 `plugin.json` 输出清单
6. 打包为 `.hmap` 分发包zip 格式,内含 `plugin.json` + 二进制)
### plg.json项目配置vs plugin.json输出清单
| 文件 | 用途 | 关键字段 |
|------|------|---------|
| `plg.json` | 项目元信息,由开发者维护 | `targets` — 构建目标(如 `"linux/amd64,windows/amd64"`|
| `plugin.json` | 构建产物清单,`plugindev build` 自动生成 | `entry` — 入口文件名;`platforms` — 声明的支持平台 |
每个目标生成单独的 `.hmap`,二进制文件名由平台决定:
| 平台 | 二进制 |
|------|--------|
| Linux | `plugin.so` |
| macOS | `plugin.dylib` |
| Windows | `plugin.dll` |
### 多平台打包:--bundle
```bash
plugindev build --bundle
```
一次编译 linux/amd64 + darwin/amd64 + windows/amd64生成包含所有平台二进制的单 `.hmap`
输出清单自动添加 `platforms` 字段。安装时核心自动选择当前平台的二进制,跳过其他平台。
输出在 `dist/` 目录:
```
dist/
├── myplugin_linux_amd64.hmap # 单平台Linux 版
├── myplugin_windows_amd64.hmap # 单平台Windows 版
├── myplugin_darwin_amd64.hmap # 单平台macOS 版
├── myplugin_bundle.hmap # 多平台合集
└── myplugin_lua.hmap # Lua 插件
```
### 安装部署
通过 PluginMgr HTTP API 安装,支持三种方式:
```bash
# 1. 从 URL 安装(自动清理安装包)
curl -X POST http://127.0.0.1:9876/plugins \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/myplugin.hmap"}'
# 2. 从本地路径安装(保留安装包)
curl -X POST http://127.0.0.1:9876/plugins \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/myplugin.hmap"}'
# 3. 直接上传二进制
curl -X POST http://127.0.0.1:9876/plugins \
--data-binary @dist/myplugin.hmap
```
安装后需调用 `/api/v1/plugins/reload` 或重启内核生效。
也可通过 WebUI 插件管理页面上传安装。
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 二、Go 插件开发详解
### 插件接口
```go
package main
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
type Plugin struct {
name string
sdk *sdk.PluginSDK
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
// 注册配置项、工具、阶段钩子等
return nil
}
func (p *Plugin) Stop() error {
// 清理资源
return nil
}
// NewPluginFactory 创建插件实例(由 main.go 或 Windows bridge 调用)
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
```
### 入口点
`plugindev init` 生成的 `plugin.go` 中直接包含 `NewPlugin` 导出函数,它是内核加载插件时的入口:
```go
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
```
编译时 `plugindev build` 根据目标平台自动生成 C ABI bridge 代码(`z_bridge_gen.go` + `z_entry.c`无需手动编写。Windows DLL 和 Linux/macOS .so 共享同一入口。
### PluginSDK 核心 API
#### 工具注册 — 让 LLM 可调用你的能力
```go
s.RegisterTool("weather_query", sdk.ToolDef{
Name: "weather_query",
Description: "查询指定城市的天气",
NoMemory: false, // false=输出参与记忆计算true=跳过计算
// Cleaner: func(output string) string { // 可选:输出参与向量化/jieba/蒸馏前的清洗
// return extractJSON(output, "content")
// },
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"description": "城市名称,如 北京",
},
},
"required": []string{"city"},
},
}, func(args map[string]interface{}) (interface{}, error) {
city, _ := args["city"].(string)
return map[string]interface{}{
"city": city,
"temp": 25,
"weather": "晴",
}, nil
})
```
##### NoMemory 与 Cleaner 说明
`NoMemory``Cleaner``ToolDef` 上的两个可选字段,控制工具输出在**记忆计算层**向量化、jieba 分词、蒸馏)中的行为:
- **`NoMemory`**(默认 `false`):设为 `true` 时,工具输出不参与任何记忆计算(向量、分词、蒸馏),但原文保留在 Context 和 Document 中LLM 注意力不受影响。适用场景:`cmd_run`(命令输出含不可控噪音)、文件上传/删除等纯操作工具。
- **`Cleaner`**(可选):函数签名 `func(output string) string`。注册后,工具输出在参与向量化/jieba/蒸馏前先经过此函数过滤。典型用途SQL 查询去前缀、JSON 包裹提取 `content` 字段。原文始终不变Cleaner 只影响计算层输入。
决策矩阵:
```
工具输出 → 对 LLM 注意力有信号价值?
├── 否 → NoMemory=true输出保留原文跳过计算层
└── 是 → 有可控噪音?
├── 是 → Cleaner 过滤后参与计算
└── 否 → 正常记忆,无需额外处理
```
> **注意**`Cleaner` 是 Go `func` 类型(`json:"-"`),不能跨 C ABI 边界序列化。Lua 插件和远程插件无法使用。
#### 阶段钩子 — 干预消息处理流
7 个阶段:
| 阶段 | 时机 | 用途 |
|------|------|------|
| `on_input` | 消息刚到达 Agent | 黑名单、限流、短路 |
| `pre_action` | 即将调用 LLM | 注入上下文 |
| `post_action` | LLM 返回结果 | 修改输出/工具列表 |
| `before_toolcall` | 工具调用前 | 审计、拒绝、改参 |
| `after_toolcall` | 工具执行后 | 脱敏、改写结果 |
| `before_output` | 输出前 | 格式适配、泄漏清洗 |
| `after_output` | 输出后 | 统计日志 |
```go
// 全局监听:所有插件的阶段事件
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
ctx.Lock()
ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{
"role": "system",
"content": "注入的上下文内容",
})
ctx.Unlock()
return nil
})
// 仅自己工具:仅监听自己注册的 tool 的 before_toolcall/after_toolcall
s.RegisterStage(sdk.StageBeforeToolcall, myHandler, sdk.StageScopeOwnTools)
```
#### 配置管理
```go
// 注册配置项定义
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "plugin.myplugin.api_key",
Default: "",
Type: "string",
DisplayName: "API Key",
Description: "API 密钥",
Category: "myplugin",
})
// 读写配置
val, err := s.Settings().Get("api_key")
s.Settings().Set("api_key", "new-value")
// 读取核心配置
s.Settings().GetCore("llm.model")
// 读取其他插件配置
s.Settings().GetPlugin("other_plugin", "some_key")
```
#### 输入投递
```go
// 普通投递(按序处理)
s.InjectText(source, channel, text string)
// 中断投递(可打断当前 LLM 处理)
s.InjectInterruptText(source, channel, text string)
// 不记入记忆
s.InjectTextNoMemory(source, channel, text string)
```
#### 事件订阅
```go
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
unsub := s.Events().Subscribe(sdk.EventToolCall, func(evt *sdk.Event) {
log.Printf("工具被调用: %v", evt.Payload)
})
defer unsub()
```
#### 能力访问
```go
// 图记忆(实体-关系存储)
entities, relations, err := s.Memory().Recall([]string{"关键词"}, 2)
// 文档记忆(向量存储)
docs := s.DocMemory().Query("查询文本", 3)
// 知识库
results, err := s.Knowledge().Search("查询", 5)
// LLM 源管理
s.LLM().ListSources() // 返回 []string
s.LLM().SetSource("deepseek")
```
#### 事件订阅(内置插件)
```go
// 订阅系统事件,返回取消订阅函数
unsub := s.Subscribe("tool_call", func(evt *events.Event) {
log.Printf("工具被调用: %v", evt.Payload)
})
defer unsub()
// 发布事件
s.Publish(&events.Event{
Type: "custom_event",
Payload: map[string]interface{}{"key": "value"},
})
```
#### IO 通道管理(内置插件)
```go
// 注册通道绑定设备驱动dev 必须实现 agentIO.Device 接口:
// Name() string
// Type() DeviceType
// Description() string
// Tools() []ToolDef
// Execute(tool string, args map[string]interface{}) (interface{}, error)
// Start() error
// Stop() error
// OutputCapabilities() OutputCapability
s.RegisterChannel("mydevice", deviceImpl)
// 注销通道
s.UnregisterChannel("mydevice")
// 列出所有通道
channels := s.ListChannels()
```
#### 输入投递(内置插件)
```go
// 排队投递(按序处理)
s.InjectInput(source, channel, eventType string, payload map[string]interface{})
// 同步投递(等待响应)
resp := s.InjectInputSync(source, channel, eventType string, payload map[string]interface{})
// 中断投递(可打断当前 LLM 处理)
s.InjectInterrupt(source, channel, eventType string, payload map[string]interface{})
// 同步文本投递(快捷方式)
resp := s.InjectTextSync(source, channel, text string)
resp := s.InjectTextSyncNoMemory(source, channel, text string)
// 获取输出通道
outputCh := s.OutputChan()
```
> **注意**`Subscribe`、`Publish`、`RegisterChannel`、`UnregisterChannel`、`ListChannels`、`InjectInput`、`InjectInputSync`、`InjectInterrupt`、`InjectTextSync`、`InjectTextSyncNoMemory`、`OutputChan` 这些方法仅在内置插件中可用(`internal/sdk` 包),外部动态插件无法访问。外部插件请使用 `InjectText`、`InjectInterruptText`、`InjectTextNoMemory` 等公共 API。
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 三、Lua 插件开发详解
Lua 插件适合轻量级快速原型,无需 Go 编译环境,修改后直接重启内核即可生效。
### 插件结构
```lua
-- main.lua
local plugin = {
name = "myluaplugin"
}
function plugin.start(sdk)
sdk.log("info", "myluaplugin starting...")
sdk.register_tool("myluaplugin_hello", {
description = "A hello world tool",
parameters = {
type = "object",
properties = {}
}
}, function(args)
return { content = "Hello from myluaplugin plugin!" }
end)
sdk.log("info", "myluaplugin started")
end
function plugin.stop()
sdk.log("info", "myluaplugin stopped")
end
return plugin
```
### SDK 模拟层
`sdk.lua` 提供纯 Lua 的 SDK 模拟实现,支持 `lua main.lua` 独立测试:
```bash
lua main.lua
# 输出:
# [lua-plugin] info: myluaplugin starting...
# [lua-plugin] register_tool: myluaplugin_hello
# [lua-plugin] info: myluaplugin started
```
在内核中运行时,`sdk.*` 全局变量由 Go 层注入,所有 `-- !impl` 标记的函数会被替换为真实实现。
### Lua SDK API
| 函数 | 说明 |
|------|------|
| `sdk.log(level, msg)` | 日志输出 |
| `sdk.register_tool(name, def, handler)` | 注册工具 |
| `sdk.register_stage(stage, handler)` | 注册阶段钩子 |
| `sdk.register_api(name)` | 注册 API |
| `sdk.get_setting(key)` | 读取配置 |
| `sdk.set_setting(key, value)` | 写入配置 |
| `sdk.inject_text(source, channel, text)` | 投递文本消息 |
| `sdk.inject_interrupt(source, channel, text)` | 中断投递 |
| `sdk.json.encode(val)` | JSON 编码 |
| `sdk.json.decode(str)` | JSON 解码 |
| `sdk.http.get(url)` | HTTP GET 请求(`-- !impl` |
| `sdk.http.post(url, body, content_type)` | HTTP POST 请求(`-- !impl` |
> **注意**Lua 插件的 `sdk.register_stage` 阶段回调目前仅传递 `raw_message`、`user_id`、`phase` 三个字段,功能受限。复杂的阶段处理逻辑建议使用 Go 插件。
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 四、内置插件
内置插件使用 `init()` 自注册方式,编译进内核,无需单独部署。
### 目录结构
```
internal/plugins/yourplugin/
plugin.go — 插件主文件
```
### 最小插件示例
```go
package yourplugin
import (
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func init() {
plugin.RegisterFactory("yourplugin", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
})
}
type Plugin struct {
name string
}
func New(name string) *Plugin {
return &Plugin{name: name}
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
// 在这里初始化插件:启动 goroutine、注册工具、订阅事件等
return nil
}
func (p *Plugin) Stop() error {
// 清理资源
return nil
}
```
### 注册到内核
`internal/plugins/all.go` 中添加空白导入:
```go
package plugins
import (
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/yourplugin"
// ... 其他插件
)
```
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 五、最佳实践
1. `Start()` 非阻塞 — goroutine 启动长任务,不要阻塞 Start
2. `Stop()` 清理资源 — 关连接、停 goroutine、取消订阅
3. 工具名唯一 — 建议插件名前缀避免冲突
4. handler 返回 `error` 时 LLM 会收到并可能重试
5. 打断用 `InjectInterruptText`,普通投递用 `InjectText`
6. 配置用 `Settings().Get/Set`,不要硬编码
7. 外部 Go 插件独立编译,不依赖内核版本;内置插件才需随内核重新编译
---
<img src="../../assets/branding/mascot-xiaozhai.webp" width="20" style="border-radius:50%;vertical-align:middle"> :
## 六、示例插件参考
### SDK 仓库示例(`homeagent-sdk/example/`
| 示例 | 类型 | 特点 |
|------|------|------|
| [memo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/memo) | Go | 备忘管理PreAction 注入 + 定时打断双提醒 |
| [files](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/files) | Go | 文件系统操作4 种写入模式,沙箱隔离 |
| [browser](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/browser) | Go | 网络搜索、网页抓取SSRF、浏览器渲染合并自 web/webfetch |
| [bili](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/bili) | Go | B 站视频下载yt-dlp |
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot 对接17 个工具 |
| [editdoc](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/editdoc) | Go | Office 文档编辑与格式转换 |
| [a2a](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/a2a) | Go | Agent-to-Agent 协议 |
| [ocr](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ocr) | Go | 离线文字识别Tesseract |
| [sanitizer](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/sanitizer) | Go | 输出清洗过滤器 |
### 内置插件
| 插件 | 位置 | 特点 |
|------|------|------|
| Timer | `internal/plugins/timer/` | 最简单的完整示例,注册一个工具 + 中断反馈 |
| CLI | `internal/plugins/cli/` | Unix socket 监听 + 同步请求响应 |
| WebUI | `internal/plugins/webui/` | HTTP 服务 + 依赖注入 |
---
*了解项目整体目标?查看 [OVERVIEW.md](OVERVIEW.md)。*
*了解技术架构?查看 [ARCHITECTURE.md](ARCHITECTURE.md)。*
*SDK 仓库与开发工具?查看 [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk)。*