mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
feat: 实现多语言文档架构
- 创建 docs/zh/ 和 docs/en/ 目录结构 - 迁移文档至多语言目录,添加中英文切换链接 - 创建 README_EN.md 英文版本 - 更新 README.md 添加语言切换链接 - 删除旧文档和临时文件
This commit is contained in:
104
docs/zh/ADAPTER.md
Normal file
104
docs/zh/ADAPTER.md
Normal file
@ -0,0 +1,104 @@
|
||||
[English](../en/ADAPTER.md) | **中文**
|
||||
|
||||
# Lua Adapter — LLM 源适配指南
|
||||
|
||||
每个 LLM API 源对应一个 Lua 脚本,负责请求转换(Go 统一格式 → API 格式)和响应转换(API 格式 → Go 统一格式)。
|
||||
|
||||
## 适配器契约
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 统一 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"
|
||||
}
|
||||
```
|
||||
|
||||
## 统一 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" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Lua VM 内置函数
|
||||
|
||||
`json.encode(table)` — 将 Lua table 编码为 JSON 字符串
|
||||
|
||||
`json.decode(string)` — 将 JSON 字符串解码为 Lua table
|
||||
|
||||
`log(level, message)` — 输出日志(level: info/warn/error)
|
||||
|
||||
## 适配典型 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 API,system 消息分离,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 格式 |
|
||||
|
||||
## 添加新源步骤
|
||||
|
||||
1. 在 `internal/lua/adapters/` 下创建 `<name>.lua`
|
||||
2. 脚本定义 `transform_request` 和 `transform_response`
|
||||
3. (可选)定义 `transform_stream_chunk` 支持流式
|
||||
4. 编译验证:`go build ./cmd/homed/`
|
||||
5. 测试验证:`go test ./...`
|
||||
347
docs/zh/ARCHITECTURE.md
Normal file
347
docs/zh/ARCHITECTURE.md
Normal file
@ -0,0 +1,347 @@
|
||||
[English](../en/ARCHITECTURE.md) | **中文**
|
||||
|
||||
# HomeAgent 架构
|
||||
|
||||
内核零 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/agent.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: 每次输入, Vectorize(char 1-2gram TF-IDF)
|
||||
Prune: TF-IDF CosineSimilarity, 保留 topK + 最近10条
|
||||
├── 保留 → timeline → system prompt (按时间排序)
|
||||
└── 低分 → Document 层归档 (ContextToDoc)
|
||||
Save: 5s debounce 写盘
|
||||
|
||||
↓ Prune 归档 ↑ LLM 主动召回
|
||||
|
||||
② Document (文件记忆)
|
||||
DocStore — JSON文件 + TF-IDF InvertedIndex
|
||||
写入: Prune归档 / doc_commit / Graph快照(syncGraphToDocs)
|
||||
读取:
|
||||
├── 自动注入: Query(input, top3) → 【相关记忆文档】→ system prompt (只读, 更新 AccessCount)
|
||||
└── 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 规则蒸馏
|
||||
读取:
|
||||
├── 自动召回: Indexer.BuildContext(input)
|
||||
│ → TF-IDF 实体名搜索 → BFS depth=2
|
||||
│ → 【记忆索引】→ system prompt
|
||||
└── LLM主动: memory_recall / doc_query
|
||||
Social: person_query / set_trait / relate (包装 GraphDB)
|
||||
|
||||
④ 蒸馏管道 (每30min心跳)
|
||||
distillContext → 窗口>2×maxSize → 强制Prune
|
||||
syncGraphToDocs → Graph 快照写入 Document(跨层可搜索)
|
||||
reorgGraph:
|
||||
Step1: indexer.Sync — 重建实体TF-IDF向量索引
|
||||
Step2: docStore.Reindex — 重建文档TF-IDF向量索引
|
||||
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 向量化(char 1-2 gram)
|
||||
|
||||
TF-IDF 是贯穿三层记忆的核心算法,在 4 个独立位置以不同方式使用:
|
||||
|
||||
| 位置 | 文件 | 用途 | 算法 |
|
||||
|------|------|------|------|
|
||||
| Context Prune | `context.go:162` | 裁剪低相关性上下文事件 | CosineSimilarity(queryVec, evt.Vector) |
|
||||
| DocStore Query | `document.go:205` | 从文档记忆召回相关内容 | InvertedIndex + CosineSimilarity |
|
||||
| Indexer 实体搜索 | `indexer.go:149` | 从Graph召回相关实体 | InvertedIndex + CosineSimilarity |
|
||||
| 实体相似度检测 | `agent.go:2297` | 检测Graph中相似实体 | Bigram Jaccard (>0.75 → consolidation) |
|
||||
|
||||
### Context 层
|
||||
|
||||
`internal/agent/core/context.go` — `RelevanceContext`
|
||||
- 维护最近事件列表,每次 Append/Prune 写入 JSON 防丢
|
||||
- 用户输入时做 TF-IDF 相关性评分,保留 topK
|
||||
|
||||
### Document 层
|
||||
|
||||
`internal/memory/document/doc.go` — `Store`
|
||||
- 消费即删模式:`doc_query` 检索到后删除
|
||||
- TF-IDF 索引 character bigram + 倒排
|
||||
|
||||
### Graph 层
|
||||
|
||||
`internal/memory/graph.go` — `GraphDB`
|
||||
- SQLite WAL 模式,两张表
|
||||
- `Commit(triples)` — UPSERT entities + INSERT relations
|
||||
- `Recall(keywords, depth)` — 关键词 LIKE 搜索 + BFS 遍历
|
||||
|
||||
### 记忆工具(LLM 可直接调用)
|
||||
|
||||
| 工具 | 作用 |
|
||||
|------|------|
|
||||
| `memory_recall` | 从 Graph 召回 |
|
||||
| `memory_commit` | 写入 Graph 三元组 |
|
||||
| `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`) — 实体向量化,自动注入 system prompt
|
||||
|
||||
### 蒸馏管道
|
||||
|
||||
`internal/memory/pipeline/pipeline.go`
|
||||
- 10 分钟 tick,7 天保留
|
||||
- 规则提取三元组(name / location / likes / age / job 模式)
|
||||
- 写入 GraphDB
|
||||
|
||||
### 上下文剪枝
|
||||
|
||||
```
|
||||
心跳 30min:
|
||||
├── distillContext() — 蒸馏当前上下文
|
||||
├── syncGraphToDocs() — Graph→Document 同步
|
||||
└── reorgGraph()
|
||||
├── Indexer.Sync()
|
||||
├── DocStore.Reindex()
|
||||
├── 冷文档→Graph
|
||||
└── 实体冲突 → enqueueConsolidationTask()
|
||||
│
|
||||
selfInputCh → LLM 判断合并/跳过
|
||||
```
|
||||
|
||||
实体冲突检测启发式(bigram Jaccard > 0.5),走 `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`/`.dll` | `plugin.Open` 动态加载 | `-buildmode=plugin` | qq/files/web/memo 等 |
|
||||
| Lua 脚本插件 | 解析 `main.lua` 注册工具 | 无需编译,热加载 | luaplugintest/testlua 等 |
|
||||
| SKILL 插件 | 解析 `SKILL.md` | Markdown 定义 | OpenClaw 兼容 |
|
||||
|
||||
内置插件注册:`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) ──→ runStage() 在对应阶段调用
|
||||
Subscribe(event, fn) ──→ Publish() 通知所有订阅者
|
||||
```
|
||||
|
||||
`internal/sdk/` 桥接外部 SDK 接口到内核,定义完整 PluginSDK:
|
||||
|
||||
```go
|
||||
sdk.RegisterTool(name, def, handler)
|
||||
sdk.RegisterStage(stage, handler)
|
||||
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
|
||||
```
|
||||
|
||||
### Plugin 接口
|
||||
|
||||
```go
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Start(sdk *PluginSDK) error
|
||||
Stop() error
|
||||
}
|
||||
```
|
||||
|
||||
## 中断机制
|
||||
|
||||
```
|
||||
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/agent.go` — `interceptLoop` / `drainInterrupt`
|
||||
|
||||
## 配置系统
|
||||
|
||||
`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)
|
||||
│ ├── 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 协议
|
||||
│ ├── openclaw/ — OpenClaw 兼容
|
||||
│ ├── agentcli/ — PTY 终端
|
||||
│ ├── healthcheck/ — 健康检查
|
||||
│ └── pluginmgr/ — 插件管理器
|
||||
├── internal/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/doc.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/ — 守护进程管理
|
||||
├── snapshot/ — 快照
|
||||
└── tokenizer/ — 中文分词 (jieba 包装)
|
||||
```
|
||||
74
docs/zh/OVERVIEW.md
Normal file
74
docs/zh/OVERVIEW.md
Normal file
@ -0,0 +1,74 @@
|
||||
[English](../en/OVERVIEW.md) | **中文**
|
||||
|
||||
# HomeAgent — 项目概览
|
||||
|
||||
## 这是什么
|
||||
|
||||
HomeAgent 是一个持续运行的个人智能 Agent 框架。
|
||||
|
||||
核心架构:一个长时间运行的内核进程(`homed`),通过插件系统接入各种 IO 通道(QQ、Web、命令行等)。内核负责 LLM 调用编排、记忆管理、知识检索;插件负责所有外部 IO——收发消息、执行文件操作、搜索网络等。
|
||||
|
||||
### 核心创新
|
||||
|
||||
**核心域与应用域分离** — 这是首个明确提出这一划分的 Agent 框架。内核(核心域)不做任何 IO,所有 IO 能力归属插件(应用域)。边界通过 PluginSDK 明确定义:
|
||||
- 插件向内核注册工具(Tool),供 LLM 调用
|
||||
- 插件挂入处理管道(Stage),在各阶段拦截/改写消息流
|
||||
- 插件订阅/发布事件(Event),松耦合通信
|
||||
- 插件通过 IO API 排队或打断投递输入
|
||||
|
||||
这一划分的意义:内核保持纯粹(零 IO,只做编排和记忆),插件保持灵活(各司其职,热加载),互不污染。
|
||||
|
||||
**三层记忆架构** — 解决 Agent 长期运行的记忆衰减:
|
||||
- **Context 层**:内存中 TF-IDF 评分的事件窗口,实时维护最近上下文,低相关性事件自动下沉到下一层
|
||||
- **Document 层**:JSON 文件 + TF-IDF 向量索引的临时记忆,支持显式提交和隐式归档,冷数据蒸馏到 Graph
|
||||
- **Graph 层**:SQLite 图数据库,持久化实体(entities)和关系(relations),BFS 遍历召回,蒸馏管道从对话中提取三元组
|
||||
|
||||
三层递进:上下文 → 冷归档 → 长期图记忆,确保 Agent 长时间运行不退化。
|
||||
|
||||
## 它实际做了什么
|
||||
|
||||
代码位于项目仓库根目录,Go 语言实现。
|
||||
|
||||
**内核** (`internal/agent/core/agent.go`):
|
||||
- 维护一个消息循环(`eventLoop`),从 IO 层排队接收输入
|
||||
- 每次输入走完整的处理管道:记忆召回 → 人格注入 → LLM 调用 → 工具执行 → 输出发送
|
||||
- LLM 调用通过 Provider 接口抽象,支持 8 个 LLM 源自动降级
|
||||
- 上下文管理(`context.go`)基于 TF-IDF 评分,自动剪枝低相关性事件
|
||||
|
||||
**记忆系统** (`internal/memory/`):
|
||||
- **GraphDB** (`graph.go`) — SQLite,entities + relations 表,BFS 遍历
|
||||
- **Document Store** (`document/doc.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=plugin` 编译为 `.so`/`.dll`,通过 `plugin.Open` 动态加载;也支持 Lua 脚本插件
|
||||
- PluginSDK (`internal/sdk/`) 定义三通道:RegisterTool / RegisterStage / Subscribe
|
||||
- 阶段钩子 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(本地)、LuaAdaptedProvider(Lua 胶水适配)
|
||||
- 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`
|
||||
|
||||
## 项目状态
|
||||
|
||||
核心功能已可运行。插件系统和 SDK 已就绪,可独立开发外部插件。
|
||||
|
||||
- 内置插件:webui / cli / timer / cmd / mcp / agentcli / healthcheck / pluginmgr / openclaw / files
|
||||
- 外部插件示例([homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) 仓库 `example/`,含 Go 和 Lua 两种类型):qq / files / web / memo / bili / editdoc / a2a / ocr / sanitizer / luaplugintest / testlua
|
||||
- 打包分发:`.hmap` 插件包格式,通过 WebUI 安装
|
||||
478
docs/zh/PLUGIN_DEV.md
Normal file
478
docs/zh/PLUGIN_DEV.md
Normal file
@ -0,0 +1,478 @@
|
||||
[English](../en/PLUGIN_DEV.md) | **中文**
|
||||
|
||||
# HomeAgent 插件开发指南
|
||||
|
||||
## 概述
|
||||
|
||||
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` 生成 |
|
||||
|
||||
---
|
||||
|
||||
## 一、快速开始:使用 plugindev 工具链
|
||||
|
||||
`plugindev` 是 SDK 仓库提供的统一插件开发工具链,支持 Go 和 Lua 两种插件类型。
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
cd homeagent-sdk/tools/plugindev
|
||||
go build -o plugindev.exe
|
||||
# 将 plugindev.exe 加入 PATH 或直接使用
|
||||
```
|
||||
|
||||
### 创建 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 — 插件元信息(名称、版本、入口、目标平台)
|
||||
├── main.go — 入口点(非 Windows 或非 cgo 时编译)
|
||||
├── plugin.go — 插件实现(Plugin 接口)
|
||||
├── go.mod — Go 模块定义
|
||||
└── README.md — 说明文档
|
||||
```
|
||||
|
||||
**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` 确定目标平台
|
||||
2. **Go 插件**:执行 `go build -buildmode=plugin`(Linux)或 `-buildmode=c-shared`(Windows)
|
||||
3. **Lua 插件**:直接打包源码,无需编译
|
||||
4. 生成 `plugin.json` 清单文件
|
||||
5. 打包为 `.hmap` 分发包(zip 格式,内含 `plugin.json` + `plugin.so`/`plugin.dll`/`main.lua`)
|
||||
|
||||
输出在 `dist/` 目录:
|
||||
```
|
||||
dist/
|
||||
├── myplugin_linux_amd64.hmap # Go 插件 Linux 版
|
||||
├── myplugin_windows_amd64.hmap # Go 插件 Windows 版
|
||||
└── myplugin_lua.hmap # Lua 插件
|
||||
```
|
||||
|
||||
### 安装部署
|
||||
|
||||
通过 PluginMgr HTTP API 安装:
|
||||
|
||||
```bash
|
||||
# 内核 PluginMgr 监听 :9876
|
||||
curl -X POST http://127.0.0.1:9876/plugins \
|
||||
-F "file=@dist/myplugin_linux_amd64.hmap"
|
||||
```
|
||||
|
||||
或通过 WebUI 插件管理页面上传安装。
|
||||
|
||||
---
|
||||
|
||||
## 二、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
|
||||
}
|
||||
```
|
||||
|
||||
### 入口点
|
||||
|
||||
`main.go` 提供了 `NewPlugin` 导出函数,它是内核加载插件时的入口:
|
||||
|
||||
```go
|
||||
//go:build !windows || !cgo
|
||||
|
||||
package main
|
||||
|
||||
import "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return NewPluginFactory(name, config)
|
||||
}
|
||||
```
|
||||
|
||||
对于 Windows `-buildmode=c-shared`,`plugindev build` 自动生成 C ABI bridge 代码,无需手动处理。
|
||||
|
||||
### PluginSDK 核心 API
|
||||
|
||||
#### 工具注册 — 让 LLM 可调用你的能力
|
||||
|
||||
```go
|
||||
s.RegisterTool("weather_query", sdk.ToolDef{
|
||||
Name: "weather_query",
|
||||
Description: "查询指定城市的天气",
|
||||
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
|
||||
})
|
||||
```
|
||||
|
||||
#### 阶段钩子 — 干预消息处理流
|
||||
|
||||
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
|
||||
})
|
||||
```
|
||||
|
||||
#### 配置管理
|
||||
|
||||
```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.InjectInput(source, channel string, payload map[string]interface{})
|
||||
|
||||
// 中断投递(可打断当前 LLM 处理)
|
||||
s.InjectInterrupt(source, channel string, payload map[string]interface{})
|
||||
|
||||
// 快捷方式
|
||||
s.InjectText(source, channel, text string)
|
||||
s.InjectInterruptText(source, channel, text string)
|
||||
```
|
||||
|
||||
#### 事件订阅
|
||||
|
||||
```go
|
||||
unsub := s.Subscribe("tool_call", func(evt *events.Event) {
|
||||
log.Printf("工具被调用: %v", evt.Payload)
|
||||
})
|
||||
defer unsub()
|
||||
```
|
||||
|
||||
#### 能力访问
|
||||
|
||||
```go
|
||||
// 记忆
|
||||
s.Memory().Recall(query string) ([]MemItem, error)
|
||||
s.Memory().Commit(triples []Triple) error
|
||||
|
||||
// 知识
|
||||
s.Knowledge().Search(query string) ([]string, error)
|
||||
|
||||
// LLM 源管理
|
||||
s.LLM().ListSources() []SourceInfo
|
||||
s.LLM().SetSource(name string) error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、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`) |
|
||||
|
||||
---
|
||||
|
||||
## 四、内置插件
|
||||
|
||||
内置插件使用 `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"
|
||||
// ... 其他插件
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、最佳实践
|
||||
|
||||
1. `Start()` 非阻塞 — goroutine 启动长任务,不要阻塞 Start
|
||||
2. `Stop()` 清理资源 — 关连接、停 goroutine、取消订阅
|
||||
3. 工具名唯一 — 建议插件名前缀避免冲突
|
||||
4. handler 返回 `error` 时 LLM 会收到并可能重试
|
||||
5. 打断用 `InjectInterruptText`,普通投递用 `InjectText`
|
||||
6. 配置用 `Settings().Get/Set`,不要硬编码
|
||||
7. 外部 Go 插件独立编译,不依赖内核版本;内置插件才需随内核重新编译
|
||||
|
||||
---
|
||||
|
||||
## 六、示例插件参考
|
||||
|
||||
### 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 种写入模式,沙箱隔离 |
|
||||
| [web](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/web) | Go | DuckDuckGo 搜索 + 网页抓取,SSRF 防护 |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot 对接,17 个工具 |
|
||||
| [bili](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/bili) | Go | B 站视频下载(you-get) |
|
||||
| [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 | 输出清洗过滤器 |
|
||||
| [luaplugintest](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/luaplugintest) | Lua | Lua 插件 Hello World |
|
||||
| [testlua](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/testlua) | Lua | Lua 插件示例 |
|
||||
|
||||
### 内置插件
|
||||
|
||||
| 插件 | 位置 | 特点 |
|
||||
|------|------|------|
|
||||
| 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)。*
|
||||
Reference in New Issue
Block a user