mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-07 16:14:30 +00:00
refactor: migrate built-in plugins to SDK-only interface
- Six-phase plan complete: webui/cli/healthcheck/pluginmgr/clawhubadapter now interact with the kernel exclusively via internal/sdk interfaces; all Configure() calls and package-level global injection removed - buildSDK in internal/plugin/registry.go is the single assembly point - Add internal/sdk/events.go exporting event types/constants - Fix ProviderManager cooldown sharing: LuaAdaptedProvider.Name() now returns the source name instead of lua_<adapter>, so multiple sources sharing an adapter (single script load via shared VM AdapterCache) no longer share failure-cooldown state - Verified: build/vet/tests green, deployed to homeagent.service with full plugin capability testing via local OpenAI-compatible mock
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -30,3 +30,4 @@ third_party/homeagent-sdk/package/
|
|||||||
third_party/homeagent-sdk/.gitignore
|
third_party/homeagent-sdk/.gitignore
|
||||||
third_party/homeagent-sdk/README*
|
third_party/homeagent-sdk/README*
|
||||||
third_party/homeagent-sdk/example/
|
third_party/homeagent-sdk/example/
|
||||||
|
.codegraph/
|
||||||
|
|||||||
@ -31,10 +31,10 @@ import (
|
|||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/nlp"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/nlp"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
||||||
healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter"
|
||||||
openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
||||||
pluginmgr "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
|
||||||
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
||||||
@ -284,7 +284,7 @@ func main() {
|
|||||||
Temperature: cfg.LLM.Temperature,
|
Temperature: cfg.LLM.Temperature,
|
||||||
MaxTokens: cfg.LLM.MaxTokens,
|
MaxTokens: cfg.LLM.MaxTokens,
|
||||||
ContextWindow: src.ContextWindow,
|
ContextWindow: src.ContextWindow,
|
||||||
}, luaVM, src.Adapter)
|
}, luaVM, src.Name, src.Adapter)
|
||||||
providerMgr.Register(src.Name, luaProvider)
|
providerMgr.Register(src.Name, luaProvider)
|
||||||
}
|
}
|
||||||
if cfg.LLM.Provider != "" {
|
if cfg.LLM.Provider != "" {
|
||||||
@ -412,25 +412,30 @@ func main() {
|
|||||||
InputProcessing: cfg.InputProcessing,
|
InputProcessing: cfg.InputProcessing,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 通过 Registry 将内核依赖注入每个插件的 PluginSDK(阶段6 将替换遗留的 util.Configure)
|
||||||
|
pluginReg.SetLuaVM(luaVM)
|
||||||
|
pluginReg.SetBaseAPIKey(baseAPIKey)
|
||||||
|
pluginReg.SetSupervisor(supervisor.NewSDKAdapter(sup))
|
||||||
|
pluginReg.SetSkillManager(skMgr)
|
||||||
|
pluginReg.SetTracker(trk)
|
||||||
|
pluginReg.SetConfig(cfg)
|
||||||
|
pluginReg.SetStageHost(stageHost)
|
||||||
|
pluginReg.SetIndexer(memIdx)
|
||||||
|
pluginReg.SetStatusProvider(agent)
|
||||||
|
|
||||||
// 为内置插件注入内核依赖(各插件通过 init() 自注册工厂)
|
// 为内置插件注入内核依赖(各插件通过 init() 自注册工厂)
|
||||||
cli.DefaultSocket = *cliSocket
|
cli.DefaultSocket = *cliSocket
|
||||||
openclaw.SkillsDir = filepath.Join(cfg.Daemon.DataDir, "skills")
|
// webui 插件作为内置插件经 Registry 启动,读取自身 settings["addr"](默认 :8080)。
|
||||||
|
// 保留 CLI --webui 与 webui.listen_addr 配置对监听地址的覆盖。
|
||||||
webuiListenAddr := *httpAddr
|
webuiListenAddr := *httpAddr
|
||||||
if webuiListenAddr == "" {
|
if webuiListenAddr == "" {
|
||||||
webuiListenAddr = cfgReg.GetString("webui.listen_addr", ":8080")
|
webuiListenAddr = cfgReg.GetString("webui.listen_addr", ":8080")
|
||||||
}
|
}
|
||||||
webui.Configure(webuiListenAddr,
|
if ps := cfgReg.PluginConfig("webui"); ps != nil {
|
||||||
sup, memDB, skMgr, luaVM, cfg, iom, textMem, ks, trk, cfgReg, pluginReg, evBus, agent,
|
if v, _ := ps.Get("addr"); v == nil {
|
||||||
providerMgr, baseAPIKey,
|
_ = ps.Set("addr", webuiListenAddr)
|
||||||
)
|
}
|
||||||
healthcheck.Configure(stageHost, iom, pluginReg, memDB, ks, docStore, providerMgr, agent)
|
}
|
||||||
|
|
||||||
// Wire pluginmgr dependencies
|
|
||||||
pluginmgr.PluginDir = cfg.Plugin.Dir
|
|
||||||
pluginmgr.Reg = pluginReg
|
|
||||||
|
|
||||||
// CLI 插件结构化命令 — 直接注入内核依赖,不依赖 HTTP
|
|
||||||
cli.Configure(pluginReg, cfgReg, agent, cfg.Plugin.Dir)
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// 依存句法分析器(内嵌 ONNX 模型 / 规则引擎)
|
// 依存句法分析器(内嵌 ONNX 模型 / 规则引擎)
|
||||||
|
|||||||
377
docs/zh/plan.md
377
docs/zh/plan.md
@ -1,377 +0,0 @@
|
|||||||
# 三元组提取系统 — 施工方案
|
|
||||||
|
|
||||||
## 一、背景与目标
|
|
||||||
|
|
||||||
### 现状
|
|
||||||
|
|
||||||
- HomeAgent 已通过 systemd 托管运行,数据目录 `/home/newqqagent`
|
|
||||||
- 已积累 **62 万条原始对话记录**(125 个 raw TSV 文件)
|
|
||||||
- 当前三元组提取通过 `extractKeyTriples()` 硬编码 5 条规则完成(姓名/年龄/喜好/居住地/职业)
|
|
||||||
- `docToTriples()` 用相邻词机械拼接三元组,语义噪音大
|
|
||||||
|
|
||||||
### 目标
|
|
||||||
|
|
||||||
构建 **"句法定界 + 向量验义"** 双路三元组提取系统:
|
|
||||||
|
|
||||||
1. 用本机积累的对话语料训练一个依存句法分析模型
|
|
||||||
2. 模型以 ONNX 格式发布到 HuggingFace,Go 运行时启动时拉取
|
|
||||||
3. 依赖:Go 侧仅需 `onnxruntime_go`(纯 Go binding,无 CGO/Python)
|
|
||||||
4. 降级:模型不可用时退回现有 gojieba POS + 模板方案
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、整体架构
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ 训练流水线 (Python,一次性) │
|
|
||||||
│ │
|
|
||||||
│ /home/newqqagent/memory/raw/*.tsv │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ 数据导出 → 提取 user 语句 → 去重 → 句长过滤 │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ Baidu DDParser (教师模型) → 银标依存树 │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ UD Chinese Treebank (金标) + 银标混合 → supar 训练 │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ONNX 导出 → 上传 HuggingFace (your-org/chinese-dep-parser) │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ 推理流水线 (Go,运行时) │
|
|
||||||
│ │
|
|
||||||
│ HomeAgent 启动 │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ HuggingFace 下载 ONNX 模型 → onnxruntime_go 加载 │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ 用户输入 → gojieba 分词 + POS │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ONNX 推理 → 依存树解码 (head index + dep label) │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ 句法模板提取三元组 (SBV-VOB / SBV-IOB / ATT-VOB / ...) │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ 融合现有向量验证层 (StaticEmbedder + TransE) → 输出三元组 │
|
|
||||||
│ │
|
|
||||||
│ 模型缺失/加载失败 → 降级 gojieba POS + 模板 │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、阶段一:数据导出与探索
|
|
||||||
|
|
||||||
### 3.1 数据位置
|
|
||||||
|
|
||||||
```
|
|
||||||
/home/newqqagent/memory/raw/raw_*.tsv
|
|
||||||
格式: id \t session_id \t role \t content \t timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 导出脚本
|
|
||||||
|
|
||||||
脚本:`tools/export_conversations.py`
|
|
||||||
|
|
||||||
功能:
|
|
||||||
- 扫描所有 raw_*.tsv,提取 `role=user` 的语句
|
|
||||||
- 基础过滤:去除纯标点/极短句(<4 字),按 MD5 去重
|
|
||||||
- 输出 JSONL:`{text, session_id, timestamp, length}`
|
|
||||||
- 统计输出:句长分布直方图、总句数、唯一句数
|
|
||||||
|
|
||||||
### 3.3 DDParser 快速验证
|
|
||||||
|
|
||||||
在导出后的数据中随机抽 500 条,用 DDParser 标注后人工抽样检查:
|
|
||||||
- 依存树的句法合理性(主语/谓语/宾语是否能对齐)
|
|
||||||
- 常见错误模式(疑问句、省略句、口语化表达)
|
|
||||||
- 决定需过滤的句式黑名单(如有)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、阶段二:训练流水线搭建
|
|
||||||
|
|
||||||
### 4.1 教师模型标注
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 使用 Baidu LAC + DDParser 联合标注
|
|
||||||
# LAC:分词 + 词性标注
|
|
||||||
# DDParser:依存句法分析
|
|
||||||
|
|
||||||
文本: "我在杭州读书"
|
|
||||||
LAC → ['我', '在', '杭州', '读书'] / ['r', 'p', 'ns', 'v']
|
|
||||||
DDParser → [{'id':0,'head':2,'deprel':'SBV'}, # 我 → 在(主语)
|
|
||||||
{'id':1,'head':3,'deprel':'ADV'}, # 在 → 杭州(状语)
|
|
||||||
{'id':2,'head':3,'deprel':'ADV'}, # 杭州 → 读书(状语)
|
|
||||||
{'id':3,'head':0,'deprel':'ROOT'}] # 读书 → ROOT
|
|
||||||
```
|
|
||||||
|
|
||||||
产出格式:标准 CoNLL-U
|
|
||||||
```
|
|
||||||
1 我 _ r _ _ 2 SBV _ _
|
|
||||||
2 在 _ p _ _ 3 ADV _ _
|
|
||||||
3 杭州 _ ns _ _ 4 ADV _ _
|
|
||||||
4 读书 _ v _ _ 0 ROOT _ _
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 训练方案
|
|
||||||
|
|
||||||
**框架**: [supar](https://github.com/yzhangcs/parser) (PyTorch, BiLSTM Biaffine)
|
|
||||||
|
|
||||||
**数据组成**:
|
|
||||||
|
|
||||||
| 来源 | 句数 | 标签 | 用途 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| UD_Chinese-GSD | ~4K | 金标 | dev/test 锚点 |
|
|
||||||
| UD_Chinese-HK | ~1K | 金标 | dev/test 锚点 |
|
|
||||||
| DDParser 标注本机对话 | 10K-20K | 银标 | train 主体 |
|
|
||||||
|
|
||||||
**模型配置**:
|
|
||||||
|
|
||||||
| 参数 | 值 |
|
|
||||||
|------|-----|
|
|
||||||
| encoder | BiLSTM |
|
|
||||||
| hidden | 200 |
|
|
||||||
| layers | 3 |
|
|
||||||
| embed_dim | 50 |
|
|
||||||
| dropout | 0.33 |
|
|
||||||
| epochs | 50 (early stop) |
|
|
||||||
| batch_size | 32 |
|
|
||||||
|
|
||||||
**预期指标**:
|
|
||||||
- LAS (标注依存): ≥80 (金标测试集)
|
|
||||||
- UAS (未标注依存): ≥85 (金标测试集)
|
|
||||||
|
|
||||||
### 4.3 ONNX 导出
|
|
||||||
|
|
||||||
```python
|
|
||||||
torch.onnx.export(
|
|
||||||
model,
|
|
||||||
(input_ids, pos_ids, char_ids),
|
|
||||||
"dep_parser.onnx",
|
|
||||||
input_names=["input_ids", "pos_ids", "char_ids"],
|
|
||||||
output_names=["head_logits", "label_logits"],
|
|
||||||
dynamic_axes={"input_ids": {0: "batch", 1: "seq"}},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
模型包结构:
|
|
||||||
|
|
||||||
```
|
|
||||||
dep_parser.onnx # ~15MB
|
|
||||||
vocab.json # token → id 映射
|
|
||||||
pos_vocab.json # POS tag → id 映射
|
|
||||||
config.json # 模型超参 + 版本信息
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.4 发布到 HuggingFace
|
|
||||||
|
|
||||||
```bash
|
|
||||||
huggingface-cli upload your-org/chinese-dep-parser \
|
|
||||||
dep_parser.onnx \
|
|
||||||
vocab.json \
|
|
||||||
pos_vocab.json \
|
|
||||||
config.json \
|
|
||||||
--repo-type model
|
|
||||||
```
|
|
||||||
|
|
||||||
模型页面附加信息:
|
|
||||||
- 训练数据来源(UD + HomeAgent 对话语料)
|
|
||||||
- 模型结构与超参
|
|
||||||
- 已验证的输入/输出格式
|
|
||||||
- 降级建议
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、阶段三:Go 推理集成
|
|
||||||
|
|
||||||
### 5.1 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/nlp/
|
|
||||||
├── dep_parser.go # ONNX 模型管理 + 推理
|
|
||||||
├── decode.go # 依存解码算法(argmax + MST)
|
|
||||||
├── triple_extractor.go # 句法模板 → 三元组
|
|
||||||
├── fallback.go # gojieba POS + 模板降级
|
|
||||||
└── model.go # 数据模型定义
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 模型生命周期管理
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 启动时:
|
|
||||||
// 1. 检查 {dataDir}/models/dep_parser.onnx 是否存在
|
|
||||||
// 2. 不存在 → 从 HuggingFace 下载
|
|
||||||
// GET https://huggingface.co/your-org/chinese-dep-parser/resolve/main/dep_parser.onnx
|
|
||||||
// 3. onnxruntime_go.NewDynamicAdvancedModel() 加载
|
|
||||||
// 4. 加载失败 → 启用 fallback,日志告警
|
|
||||||
// 5. 检查可选的版本更新(按 config.json 的 version 字段)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 推理接口
|
|
||||||
|
|
||||||
```go
|
|
||||||
type DepParseResult struct {
|
|
||||||
Tokens []string // 分词结果
|
|
||||||
POS []string // 词性标签
|
|
||||||
Heads []int // 每个词的父节点索引(0=ROOT)
|
|
||||||
DepRels []string // 依存关系标签
|
|
||||||
}
|
|
||||||
|
|
||||||
type Triple struct {
|
|
||||||
Subject string
|
|
||||||
Relation string
|
|
||||||
Object string
|
|
||||||
Score float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type Extractor struct {
|
|
||||||
parser *DepParser
|
|
||||||
embed *memory.StaticEmbedder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Extractor) Extract(text string) []Triple {
|
|
||||||
// 1. DepParser.Parse(text) → DepParseResult
|
|
||||||
// 2. 句法模板匹配 → 候选三元组
|
|
||||||
// 3. 向量验证(cos(h+r, t))→ 过滤
|
|
||||||
// 4. 融合打分 → 输出
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.4 句法模板(初版)
|
|
||||||
|
|
||||||
| 模板 | 依存模式 | 先验置信度 |
|
|
||||||
|------|----------|-----------|
|
|
||||||
| SBV-VOB | `(SBV) → VOB` | 0.9 |
|
|
||||||
| SBV-IOB | `(SBV) → IOB → VOB` | 0.85 |
|
|
||||||
| ATT-VOB | `(ATT) → VOB` | 0.8 |
|
|
||||||
| SBV-POB | `(SBV) → POB` | 0.75 |
|
|
||||||
| COO 链 | 并列结构扩展 | 0.6 |
|
|
||||||
|
|
||||||
### 5.5 降级策略
|
|
||||||
|
|
||||||
| 故障场景 | 行为 |
|
|
||||||
|---------|------|
|
|
||||||
| ONNX 模型文件不存在 | 启动时下载,下载失败则进 fallback |
|
|
||||||
| onnxruntime_go 加载失败 | 日志告警 + 进 fallback |
|
|
||||||
| 单句推理超时/panic | 返回空三元组,不中断流水线 |
|
|
||||||
| 全部正常 | 优先 ONNX 模式 |
|
|
||||||
|
|
||||||
Fallback 模式沿用现有的 gojieba POS 局部模板提取(POS 序列匹配),不需要额外依赖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、阶段四:集成到现有蒸馏管线
|
|
||||||
|
|
||||||
### 6.1 修改点
|
|
||||||
|
|
||||||
| 文件 | 改动 |
|
|
||||||
|------|------|
|
|
||||||
| `internal/agent/core/distill.go` | `docToTriples()` 改用新 Extractor |
|
|
||||||
| `internal/memory/pipeline/pipeline.go` | `extractKeyTriples()` 替换为新 Extract |
|
|
||||||
| `internal/agent/core/process.go` | 系统提示注入时走新提取器(可选) |
|
|
||||||
|
|
||||||
### 6.2 蒸馏管线的三个触发点
|
|
||||||
|
|
||||||
```
|
|
||||||
1. 实时 (process.go): 用户输入经过 NLU 时,即时提取三元组写入 Graph
|
|
||||||
2. 周期蒸馏 (pipeline.go): 10 分钟心跳,批量处理 7天前的原始记录
|
|
||||||
3. 冷文档归档 (distill.go): 72h 未访问的文档 → docToTriples
|
|
||||||
```
|
|
||||||
|
|
||||||
新的 `Extractor` 在三个触发点统一使用,上游调用方无需感知底层是 ONNX 还是 fallback。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、时间线
|
|
||||||
|
|
||||||
| 阶段 | 内容 | 预估工时 |
|
|
||||||
|------|------|----------|
|
|
||||||
| 一 | 数据导出 + DDParser 快速验证 | 1 天 |
|
|
||||||
| 二 | 训练流水线搭建 + v0.1 训练 + ONNX 导出 | 2 天 |
|
|
||||||
| 三 | Go 推理集成 + 句法模板 | 2 天 |
|
|
||||||
| 四 | 蒸馏管线接入 + 降级测试 | 1 天 |
|
|
||||||
| 五 | HuggingFace 发布 + 文档 + 回测 | 1 天 |
|
|
||||||
| **总计** | | **7 天** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、模型维护策略
|
|
||||||
|
|
||||||
### 8.1 版本迭代
|
|
||||||
|
|
||||||
| 版本 | 触发条件 | 训练数据 |
|
|
||||||
|------|---------|---------|
|
|
||||||
| v0.1 | 初始版 | UD + 10K 本机对话 |
|
|
||||||
| v0.2 | 累计 50K 新对话 | 增量合并 retrain |
|
|
||||||
| v1.0 | 对话域 LAS ≥85 | 全量 + 人工抽检 |
|
|
||||||
|
|
||||||
### 8.2 更新机制
|
|
||||||
|
|
||||||
```
|
|
||||||
HomeAgent 启动 → 检查 HuggingFace 模型版本
|
|
||||||
├── 本地版本 < 远端版本 → 后台下载新模型,下次重启生效
|
|
||||||
└── 本地版本 == 远端版本 → 跳过
|
|
||||||
```
|
|
||||||
|
|
||||||
通过 `config.json` 中的 `version` 字段比对,采用先下载后原子替换的策略。
|
|
||||||
|
|
||||||
### 8.3 回滚
|
|
||||||
|
|
||||||
```
|
|
||||||
/data/newqqagent/models/
|
|
||||||
├── dep_parser.onnx # 当前版本 (symlink)
|
|
||||||
├── dep_parser_v0.1.onnx # 历史版本
|
|
||||||
└── dep_parser_v0.2.onnx # 历史版本
|
|
||||||
```
|
|
||||||
|
|
||||||
启动失败时自动 rollback 到上一个可用版本。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、与现有系统的交互
|
|
||||||
|
|
||||||
### 9.1 Context 向量层关联
|
|
||||||
|
|
||||||
之前讨论的 **TF-IDF 加权词向量平均** 与三元组提取是两条独立优化线路:
|
|
||||||
|
|
||||||
```
|
|
||||||
三元组提取 (本计划) Context 向量 (之前已改完)
|
|
||||||
───────────────── ────────────────────────
|
|
||||||
句法定界 + 向量验义 jieba 精确模式 + TF-IDF 加权
|
|
||||||
输出: (sub, rel, obj) 输出: 300d 语义向量
|
|
||||||
用于: GraphDB 写入 用于: Context 裁剪评分
|
|
||||||
```
|
|
||||||
|
|
||||||
两者共享 gojieba 分词结果和 StaticEmbedder 词向量,但不直接耦合。
|
|
||||||
|
|
||||||
### 9.2 向量验证层的复用
|
|
||||||
|
|
||||||
`StaticEmbedder` 的 `Vectorize()` 可以直接用于 TransE 验证:
|
|
||||||
```go
|
|
||||||
h := embed.Vectorize(subject)
|
|
||||||
r := embed.Vectorize(relation) // 谓语子树语义中心
|
|
||||||
t := embed.Vectorize(object)
|
|
||||||
score := CosineSimilarity(h + r, t)
|
|
||||||
```
|
|
||||||
|
|
||||||
无需额外加载词向量模型,与 Context 层在同一向量空间。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十、风险与缓解
|
|
||||||
|
|
||||||
| 风险 | 概率 | 影响 | 缓解 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| DDParser 标注质量低 | 中 | 模型学偏 | 混入 UD 金标 + 抽检 500 条先行验证 |
|
|
||||||
| 对话语料句式单一 | 中 | 泛化差 | 数据增强(依存树扰动/回译) |
|
|
||||||
| onnxruntime_go 兼容问题 | 低 | Go 侧无法加载 | fallback 模式独立完整,不影响已有功能 |
|
|
||||||
| 模型体积大 | 低 | 启动慢/占用高 | ~15MB ONNX,可接受 |
|
|
||||||
| HuggingFace 下载失败 | 低 | 首次启动受阻 | 支持本地预下载 + fallback |
|
|
||||||
@ -211,7 +211,7 @@ type LuaAdaptedProvider struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAdaptedProvider {
|
func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, name, adapter string) *LuaAdaptedProvider {
|
||||||
if cfg.Temperature == 0 {
|
if cfg.Temperature == 0 {
|
||||||
cfg.Temperature = 0.7
|
cfg.Temperature = 0.7
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAda
|
|||||||
cfg.MaxTokens = 4096
|
cfg.MaxTokens = 4096
|
||||||
}
|
}
|
||||||
return &LuaAdaptedProvider{
|
return &LuaAdaptedProvider{
|
||||||
name: fmt.Sprintf("lua_%s", adapter),
|
name: name,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
vm: vm,
|
vm: vm,
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
|
|||||||
@ -28,7 +28,7 @@ func TestQuickChatWithRealKey(t *testing.T) {
|
|||||||
Model: "deepseek-v4-flash",
|
Model: "deepseek-v4-flash",
|
||||||
BaseURL: "https://api.deepseek.com",
|
BaseURL: "https://api.deepseek.com",
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
}, vm, "deepseek"))
|
}, vm, "deepseek", "deepseek"))
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@ -21,43 +21,20 @@ type StatusProvider interface {
|
|||||||
GetKernelStatus() *KernelStatus
|
GetKernelStatus() *KernelStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// KernelStatus 内核各子系统运行状态的聚合快照。
|
// 状态 DTO 使用内置 SDK 的中立类型,保证与插件层解耦。
|
||||||
type KernelStatus struct {
|
type KernelStatus = sdk.KernelStatus
|
||||||
Uptime string `json:"uptime"`
|
type PluginInfo = sdk.PluginInfo
|
||||||
StartTime string `json:"start_time"`
|
type ChannelInfo = sdk.ChannelInfo
|
||||||
|
type MemoryStatus = sdk.MemoryStatus
|
||||||
AgentID string `json:"agent_id"`
|
type KnowledgeStatus = sdk.KnowledgeStatus
|
||||||
|
type DocumentStatus = sdk.DocumentStatus
|
||||||
Plugins []PluginInfo `json:"plugins"`
|
type TextMemoryStatus = sdk.TextMemoryStatus
|
||||||
Tools []sdk.ToolDef `json:"tools"`
|
type SocialStatus = sdk.SocialStatus
|
||||||
Channels []ChannelInfo `json:"channels"`
|
type SkillsStatus = sdk.SkillsStatus
|
||||||
|
type LLMStatus = sdk.LLMStatus
|
||||||
Memory MemoryStatus `json:"memory"`
|
type ContextStatus = sdk.ContextStatus
|
||||||
Knowledge KnowledgeStatus `json:"knowledge"`
|
type RuntimeStatus = sdk.RuntimeStatus
|
||||||
Documents DocumentStatus `json:"documents"`
|
type TrackerStatus = sdk.TrackerStatus
|
||||||
TextMemory TextMemoryStatus `json:"text_memory"`
|
|
||||||
Social SocialStatus `json:"social"`
|
|
||||||
Skills SkillsStatus `json:"skills"`
|
|
||||||
|
|
||||||
LLM LLMStatus `json:"llm"`
|
|
||||||
|
|
||||||
Context ContextStatus `json:"context"`
|
|
||||||
|
|
||||||
Runtime RuntimeStatus `json:"runtime"`
|
|
||||||
|
|
||||||
Tracker TrackerStatus `json:"tracker"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PluginInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Loaded bool `json:"loaded"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Ready bool `json:"ready"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo {
|
func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo {
|
||||||
return ChannelInfo{
|
return ChannelInfo{
|
||||||
@ -67,61 +44,6 @@ func channelInfoFromIO(ch agentIO.ChannelInfo) ChannelInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MemoryStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
EntityCount int `json:"entity_count"`
|
|
||||||
RelationCount int `json:"relation_count"`
|
|
||||||
EntityTypes int `json:"entity_types"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type KnowledgeStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
ItemCount int `json:"item_count"`
|
|
||||||
Items []string `json:"items,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DocumentStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
DocCount int `json:"doc_count"`
|
|
||||||
VectorCount int `json:"vector_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextMemoryStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
FileCount int `json:"file_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SocialStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
PersonCount int `json:"person_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SkillsStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
SkillList []string `json:"skill_list,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LLMStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
Sources int `json:"sources,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContextStatus struct {
|
|
||||||
EventCount int `json:"event_count,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuntimeStatus struct {
|
|
||||||
Goroutines int `json:"goroutines"`
|
|
||||||
MemoryMB int64 `json:"memory_mb"`
|
|
||||||
GoVersion string `json:"go_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrackerStatus struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
Dir string `json:"dir,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectKernelStatus 聚合内核各子系统状态快照。
|
// collectKernelStatus 聚合内核各子系统状态快照。
|
||||||
// 接收所有子系统引用(均为可选——nil 表示不可用),返回统一的状态报告。
|
// 接收所有子系统引用(均为可选——nil 表示不可用),返回统一的状态报告。
|
||||||
func collectKernelStatus(
|
func collectKernelStatus(
|
||||||
@ -300,3 +222,4 @@ func (a *Agent) GetKernelStatus() *KernelStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ StatusProvider = (*Agent)(nil)
|
var _ StatusProvider = (*Agent)(nil)
|
||||||
|
var _ sdk.StatusAPI = (*Agent)(nil)
|
||||||
|
|||||||
@ -18,21 +18,23 @@ import (
|
|||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||||
|
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error)
|
type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error)
|
||||||
|
|
||||||
// PluginMeta 插件显示名称元数据,来源于 plg.json / RegisterPluginMeta。
|
// PluginMeta 插件显示名称元数据,来源于 plg.json / RegisterPluginMeta。
|
||||||
type PluginMeta struct {
|
// 与内置 SDK 中的 PluginMeta 保持一致,供 PluginManager 接口返回。
|
||||||
NameZh string `json:"name_zh"`
|
type PluginMeta = sdk.PluginMeta
|
||||||
NameEn string `json:"name_en"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var globalPluginMeta sync.Map // name -> PluginMeta
|
var globalPluginMeta sync.Map // name -> PluginMeta
|
||||||
|
|
||||||
// RegisterPluginMeta 供插件包在 init() 中调用,注册显示名称。
|
// RegisterPluginMeta 供插件包在 init() 中调用,注册显示名称。
|
||||||
func RegisterPluginMeta(name, nameZh, nameEn string) {
|
func RegisterPluginMeta(name, nameZh, nameEn string) {
|
||||||
globalPluginMeta.Store(name, PluginMeta{NameZh: nameZh, NameEn: nameEn})
|
globalPluginMeta.Store(name, sdk.PluginMeta{NameZh: nameZh, NameEn: nameEn})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginMeta 查询插件的显示名称。
|
// GetPluginMeta 查询插件的显示名称。
|
||||||
@ -76,6 +78,8 @@ type Registry struct {
|
|||||||
mgr *agentAPI.ProviderManager
|
mgr *agentAPI.ProviderManager
|
||||||
cfgReg *internalConfig.ConfigRegistry
|
cfgReg *internalConfig.ConfigRegistry
|
||||||
plgDir string
|
plgDir string
|
||||||
|
lua *luaVM.VM
|
||||||
|
baseKey string
|
||||||
|
|
||||||
regTool sdk.ToolRegistrar
|
regTool sdk.ToolRegistrar
|
||||||
regStage sdk.StageRegistrar
|
regStage sdk.StageRegistrar
|
||||||
@ -83,6 +87,14 @@ type Registry struct {
|
|||||||
|
|
||||||
toolCleaner PluginToolCleaner
|
toolCleaner PluginToolCleaner
|
||||||
|
|
||||||
|
status sdk.StatusAPI
|
||||||
|
sup sdk.SupervisorAPI
|
||||||
|
skMgr *skill.Manager
|
||||||
|
trk *tracker.Tracker
|
||||||
|
cfg *types.Config
|
||||||
|
stageHost sdk.ToolSource
|
||||||
|
idx *memory.Indexer
|
||||||
|
|
||||||
knownDisabled map[string]bool
|
knownDisabled map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,10 +116,19 @@ func (r *Registry) SetKnowledge(ks *knowledge.Store) { r.ks = ks }
|
|||||||
func (r *Registry) SetProviderManager(mgr *agentAPI.ProviderManager) { r.mgr = mgr }
|
func (r *Registry) SetProviderManager(mgr *agentAPI.ProviderManager) { r.mgr = mgr }
|
||||||
func (r *Registry) SetConfigRegistry(cfgReg *internalConfig.ConfigRegistry) { r.cfgReg = cfgReg }
|
func (r *Registry) SetConfigRegistry(cfgReg *internalConfig.ConfigRegistry) { r.cfgReg = cfgReg }
|
||||||
func (r *Registry) SetPluginDir(dir string) { r.plgDir = dir }
|
func (r *Registry) SetPluginDir(dir string) { r.plgDir = dir }
|
||||||
|
func (r *Registry) SetLuaVM(vm *luaVM.VM) { r.lua = vm }
|
||||||
|
func (r *Registry) SetBaseAPIKey(key string) { r.baseKey = key }
|
||||||
func (r *Registry) SetToolRegistrar(fn sdk.ToolRegistrar) { r.regTool = fn }
|
func (r *Registry) SetToolRegistrar(fn sdk.ToolRegistrar) { r.regTool = fn }
|
||||||
func (r *Registry) SetStageRegistrar(fn sdk.StageRegistrar) { r.regStage = fn }
|
func (r *Registry) SetStageRegistrar(fn sdk.StageRegistrar) { r.regStage = fn }
|
||||||
func (r *Registry) SetAPIRegistrar(fn sdk.APIRegistrar) { r.regAPI = fn }
|
func (r *Registry) SetAPIRegistrar(fn sdk.APIRegistrar) { r.regAPI = fn }
|
||||||
func (r *Registry) SetToolCleaner(tc PluginToolCleaner) { r.toolCleaner = tc }
|
func (r *Registry) SetToolCleaner(tc PluginToolCleaner) { r.toolCleaner = tc }
|
||||||
|
func (r *Registry) SetStatusProvider(sp sdk.StatusAPI) { r.status = sp }
|
||||||
|
func (r *Registry) SetSupervisor(sup sdk.SupervisorAPI) { r.sup = sup }
|
||||||
|
func (r *Registry) SetSkillManager(skMgr *skill.Manager) { r.skMgr = skMgr }
|
||||||
|
func (r *Registry) SetTracker(trk *tracker.Tracker) { r.trk = trk }
|
||||||
|
func (r *Registry) SetConfig(cfg *types.Config) { r.cfg = cfg }
|
||||||
|
func (r *Registry) SetStageHost(sh sdk.ToolSource) { r.stageHost = sh }
|
||||||
|
func (r *Registry) SetIndexer(idx *memory.Indexer) { r.idx = idx }
|
||||||
|
|
||||||
func (r *Registry) RegisterNative(name string, factory NativeFactory) {
|
func (r *Registry) RegisterNative(name string, factory NativeFactory) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
@ -182,7 +203,7 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
|||||||
TextMemory: sdk.NewTextMemory(r.textMem),
|
TextMemory: sdk.NewTextMemory(r.textMem),
|
||||||
DocMemory: sdk.NewDocMemory(r.docStore),
|
DocMemory: sdk.NewDocMemory(r.docStore),
|
||||||
Knowledge: sdk.NewKnowledge(r.ks),
|
Knowledge: sdk.NewKnowledge(r.ks),
|
||||||
LLM: sdk.NewLLM(r.mgr),
|
LLM: sdk.NewLLM(r.mgr, r.cfgReg, r.lua, r.baseKey),
|
||||||
Settings: sett,
|
Settings: sett,
|
||||||
RegTool: regTool,
|
RegTool: regTool,
|
||||||
RegStage: regStage,
|
RegStage: regStage,
|
||||||
@ -190,6 +211,15 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
|||||||
RegOutput: regOutput,
|
RegOutput: regOutput,
|
||||||
RegInput: regInput,
|
RegInput: regInput,
|
||||||
PluginMgr: r,
|
PluginMgr: r,
|
||||||
|
|
||||||
|
Status: r.status,
|
||||||
|
Supervisor: r.sup,
|
||||||
|
Skill: r.skMgr,
|
||||||
|
Adapter: sdk.NewAdapter(r.lua),
|
||||||
|
Tracker: r.trk,
|
||||||
|
Config: sdk.NewConfig(r.cfg),
|
||||||
|
Tool: sdk.NewTool(r.stageHost, r.iom),
|
||||||
|
Indexer: sdk.NewIndexer(r.idx),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,15 +608,19 @@ func (r *Registry) ListKnown() []string {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) PluginMetas() map[string]PluginMeta {
|
func (r *Registry) PluginMetas() map[string]sdk.PluginMeta {
|
||||||
metas := make(map[string]PluginMeta)
|
metas := make(map[string]sdk.PluginMeta)
|
||||||
globalPluginMeta.Range(func(key, val interface{}) bool {
|
globalPluginMeta.Range(func(key, val interface{}) bool {
|
||||||
metas[key.(string)] = val.(PluginMeta)
|
metas[key.(string)] = val.(sdk.PluginMeta)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
return metas
|
return metas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Registry) PluginDir() string {
|
||||||
|
return r.plgDir
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
// 尝试顺序:.so (Go plugin on Linux) → .dll (Windows) → .lua (跨平台)
|
// 尝试顺序:.so (Go plugin on Linux) → .dll (Windows) → .lua (跨平台)
|
||||||
for _, try := range []struct {
|
for _, try := range []struct {
|
||||||
|
|||||||
@ -31,7 +31,12 @@ func (tc *toolCapture) RegisterAPI(name string) error {
|
|||||||
func setupPlugin() (*Plugin, *toolCapture, error) {
|
func setupPlugin() (*Plugin, *toolCapture, error) {
|
||||||
p := New("agentcli")
|
p := New("agentcli")
|
||||||
tc := newToolCapture()
|
tc := newToolCapture()
|
||||||
sdk := sdk.New("agentcli", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
sdk := sdk.New("agentcli", sdk.SDKConfig{
|
||||||
|
RegTool: tc.RegisterTool,
|
||||||
|
RegStage: tc.RegisterStage,
|
||||||
|
RegAPI: tc.RegisterAPI,
|
||||||
|
Settings: sdk.NewSettings("agentcli", nil),
|
||||||
|
})
|
||||||
if err := p.Start(sdk); err != nil {
|
if err := p.Start(sdk); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,18 +31,11 @@ var pySimulatorSrc string
|
|||||||
//go:embed simulator/openclaw_cli.js
|
//go:embed simulator/openclaw_cli.js
|
||||||
var openclawCliSrc string
|
var openclawCliSrc string
|
||||||
|
|
||||||
var SkillsDir string
|
|
||||||
var SimulatorDir string
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
plugin.RegisterPluginMeta("clawhubadapter", "ClawHub 适配器", "ClawHub Adapter")
|
plugin.RegisterPluginMeta("clawhubadapter", "ClawHub 适配器", "ClawHub Adapter")
|
||||||
plugin.RegisterFactory("clawhubadapter", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
plugin.RegisterFactory("clawhubadapter", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
dir := SkillsDir
|
dir := ""
|
||||||
if dir == "" {
|
if dataDir, ok := config["data_dir"].(string); ok && dataDir != "" {
|
||||||
dataDir, ok := config["data_dir"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("clawhubadapter plugin: config missing 'data_dir' or not a string")
|
|
||||||
}
|
|
||||||
dir = filepath.Join(dataDir, "skills")
|
dir = filepath.Join(dataDir, "skills")
|
||||||
}
|
}
|
||||||
return New(name, dir), nil
|
return New(name, dir), nil
|
||||||
@ -63,8 +56,8 @@ type Plugin struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(name, skillsDir string) *Plugin {
|
func New(name, skillsDir string) *Plugin {
|
||||||
sd := SimulatorDir
|
sd := ""
|
||||||
if sd == "" {
|
if skillsDir != "" {
|
||||||
sd = filepath.Join(skillsDir, ".simulator")
|
sd = filepath.Join(skillsDir, ".simulator")
|
||||||
}
|
}
|
||||||
return &Plugin{
|
return &Plugin{
|
||||||
@ -100,6 +93,20 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
p.simulatorDir = s
|
p.simulatorDir = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 默认目录:内核 data_dir 下 skills 目录(与配置 core.daemon.data_dir 对齐)
|
||||||
|
if p.skillsDir == "" {
|
||||||
|
if v, _ := s.Settings().GetCore("daemon.data_dir"); v != nil {
|
||||||
|
if dir, ok := v.(string); ok && dir != "" {
|
||||||
|
p.skillsDir = filepath.Join(dir, "skills")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.skillsDir == "" {
|
||||||
|
p.skillsDir = filepath.Join("data", "skills")
|
||||||
|
}
|
||||||
|
if p.simulatorDir == "" {
|
||||||
|
p.simulatorDir = filepath.Join(p.skillsDir, ".simulator")
|
||||||
|
}
|
||||||
|
|
||||||
// Launch OC plugin manager first (handles OC-format plugin installation and lifecycle)
|
// Launch OC plugin manager first (handles OC-format plugin installation and lifecycle)
|
||||||
os.MkdirAll(p.skillsDir, 0755)
|
os.MkdirAll(p.skillsDir, 0755)
|
||||||
@ -283,7 +290,7 @@ func (p *Plugin) launchManager(s *sdk.PluginSDK) error {
|
|||||||
// Ensure skills dir exists for the manager to scan
|
// Ensure skills dir exists for the manager to scan
|
||||||
os.MkdirAll(p.skillsDir, 0755)
|
os.MkdirAll(p.skillsDir, 0755)
|
||||||
|
|
||||||
sp, err := launchProcess("node", managerPath, p.skillsDir, "manager")
|
sp, err := launchProcess("node", managerPath, p.skillsDir, "manager", p.simulatorDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("launch manager: %w", err)
|
return fmt.Errorf("launch manager: %w", err)
|
||||||
}
|
}
|
||||||
@ -775,7 +782,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sp, err := launchProcess(pythonBin, simPath, dir, name)
|
sp, err := launchProcess(pythonBin, simPath, dir, name, p.simulatorDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("launch pysimulator: %w", err)
|
return fmt.Errorf("launch pysimulator: %w", err)
|
||||||
}
|
}
|
||||||
@ -808,7 +815,7 @@ func (p *Plugin) loadPySidecar(s *sdk.PluginSDK, dir, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error {
|
func (p *Plugin) loadSidecar(s *sdk.PluginSDK, dir, name string) error {
|
||||||
sp, err := launchSidecar(dir, name)
|
sp, err := launchSidecar(dir, name, p.simulatorDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("launch: %w", err)
|
return fmt.Errorf("launch: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -143,15 +143,15 @@ func (s *sidecarProcess) NotifyChan() <-chan OCNotification {
|
|||||||
return s.notifyCh
|
return s.notifyCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func launchSidecar(dir, name string) (*sidecarProcess, error) {
|
func launchSidecar(dir, name, simDir string) (*sidecarProcess, error) {
|
||||||
mainJS := filepath.Join(dir, "main.js")
|
mainJS := filepath.Join(dir, "main.js")
|
||||||
if _, err := os.Stat(mainJS); os.IsNotExist(err) {
|
if _, err := os.Stat(mainJS); os.IsNotExist(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return launchProcess("node", mainJS, dir, name)
|
return launchProcess("node", mainJS, dir, name, simDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) {
|
func launchProcess(bin, arg, dir, name, simDir string) (*sidecarProcess, error) {
|
||||||
nodePath := bin
|
nodePath := bin
|
||||||
if bin == "node" {
|
if bin == "node" {
|
||||||
if p := os.Getenv("NODE_PATH"); p != "" {
|
if p := os.Getenv("NODE_PATH"); p != "" {
|
||||||
@ -164,8 +164,8 @@ func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) {
|
|||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
// Add openclaw CLI bin dir to PATH so subprocesses can exec 'openclaw' command
|
// Add openclaw CLI bin dir to PATH so subprocesses can exec 'openclaw' command
|
||||||
if SimulatorDir != "" {
|
if simDir != "" {
|
||||||
binDir := filepath.Join(SimulatorDir, "bin")
|
binDir := filepath.Join(simDir, "bin")
|
||||||
if info, err := os.Stat(binDir); err == nil && info.IsDir() {
|
if info, err := os.Stat(binDir); err == nil && info.IsDir() {
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
binDirPath := binDir + string(os.PathListSeparator)
|
binDirPath := binDir + string(os.PathListSeparator)
|
||||||
|
|||||||
@ -26,10 +26,12 @@ func (m *mockSettings) RegisterDef(def pubsdk.ConfigDef) {}
|
|||||||
func (m *mockSettings) Defs(prefix string) []*pubsdk.ConfigDef { return nil }
|
func (m *mockSettings) Defs(prefix string) []*pubsdk.ConfigDef { return nil }
|
||||||
func (m *mockSettings) Dump() map[string]interface{} { return nil }
|
func (m *mockSettings) Dump() map[string]interface{} { return nil }
|
||||||
func (m *mockSettings) Plugins() []string { return nil }
|
func (m *mockSettings) Plugins() []string { return nil }
|
||||||
|
func (m *mockSettings) DefsCore(prefix string) []*sdk.ConfigDef { return nil }
|
||||||
|
func (m *mockSettings) DefsPlugin(plugin, prefix string) []*sdk.ConfigDef { return nil }
|
||||||
|
|
||||||
func TestLaunchSidecarNoMainJS(t *testing.T) {
|
func TestLaunchSidecarNoMainJS(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
sp, err := launchSidecar(tmpDir, "nonexistent")
|
sp, err := launchSidecar(tmpDir, "nonexistent", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@ -50,7 +52,7 @@ func TestLaunchSidecarAndListTools(t *testing.T) {
|
|||||||
t.Fatalf("write test plugin: %v", err)
|
t.Fatalf("write test plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sp, err := launchSidecar(tmpDir, "echoplugin")
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("launch sidecar: %v", err)
|
t.Fatalf("launch sidecar: %v", err)
|
||||||
}
|
}
|
||||||
@ -92,7 +94,7 @@ func TestCallEchoTool(t *testing.T) {
|
|||||||
t.Fatalf("write test plugin: %v", err)
|
t.Fatalf("write test plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sp, err := launchSidecar(tmpDir, "echoplugin")
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("launch sidecar: %v", err)
|
t.Fatalf("launch sidecar: %v", err)
|
||||||
}
|
}
|
||||||
@ -124,7 +126,7 @@ func TestCallAddTool(t *testing.T) {
|
|||||||
t.Fatalf("write test plugin: %v", err)
|
t.Fatalf("write test plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sp, err := launchSidecar(tmpDir, "echoplugin")
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("launch sidecar: %v", err)
|
t.Fatalf("launch sidecar: %v", err)
|
||||||
}
|
}
|
||||||
@ -157,7 +159,7 @@ func TestCallNonexistentTool(t *testing.T) {
|
|||||||
t.Fatalf("write test plugin: %v", err)
|
t.Fatalf("write test plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sp, err := launchSidecar(tmpDir, "echoplugin")
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("launch sidecar: %v", err)
|
t.Fatalf("launch sidecar: %v", err)
|
||||||
}
|
}
|
||||||
@ -182,7 +184,7 @@ func TestConcurrentCalls(t *testing.T) {
|
|||||||
t.Fatalf("write test plugin: %v", err)
|
t.Fatalf("write test plugin: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sp, err := launchSidecar(tmpDir, "echoplugin")
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("launch sidecar: %v", err)
|
t.Fatalf("launch sidecar: %v", err)
|
||||||
}
|
}
|
||||||
@ -217,7 +219,7 @@ func launchSimulator(t *testing.T, pluginDir, name string) *sidecarProcess {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("abs simulator path: %v", err)
|
t.Fatalf("abs simulator path: %v", err)
|
||||||
}
|
}
|
||||||
sp, err := launchProcess("node", simPath, pluginDir, name)
|
sp, err := launchProcess("node", simPath, pluginDir, name, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("launch simulator for %s: %v", name, err)
|
t.Fatalf("launch simulator for %s: %v", name, err)
|
||||||
}
|
}
|
||||||
@ -338,8 +340,6 @@ func TestLoadOCPluginViaPluginStart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SimulatorDir = filepath.Join(t.TempDir(), ".simulator")
|
|
||||||
|
|
||||||
p := New("openclaw", skillsDir)
|
p := New("openclaw", skillsDir)
|
||||||
|
|
||||||
var registeredTools []string
|
var registeredTools []string
|
||||||
|
|||||||
@ -12,8 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
|
||||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
@ -21,22 +19,6 @@ import (
|
|||||||
// DefaultSocket 由 main.go 在 Load() 前设置,覆盖默认 socket 路径。
|
// DefaultSocket 由 main.go 在 Load() 前设置,覆盖默认 socket 路径。
|
||||||
var DefaultSocket string
|
var DefaultSocket string
|
||||||
|
|
||||||
// 以下通过 Configure() 注入内核依赖
|
|
||||||
var (
|
|
||||||
pluginReg *plugin.Registry
|
|
||||||
cfgReg *internalConfig.ConfigRegistry
|
|
||||||
statusProv agentCore.StatusProvider
|
|
||||||
pluginDir string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Configure 由 main.go 在 Load() 前调用,注入内核依赖供结构化命令使用。
|
|
||||||
func Configure(pr *plugin.Registry, cr *internalConfig.ConfigRegistry, sp agentCore.StatusProvider, pDir string) {
|
|
||||||
pluginReg = pr
|
|
||||||
cfgReg = cr
|
|
||||||
statusProv = sp
|
|
||||||
pluginDir = pDir
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
plugin.RegisterPluginMeta("cli", "CLI", "CLI")
|
plugin.RegisterPluginMeta("cli", "CLI", "CLI")
|
||||||
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
@ -189,18 +171,16 @@ func (p *Plugin) cliAPIKey(s *sdk.PluginSDK) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p.webuiAPIKey()
|
return p.webuiAPIKey(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) webuiAPIKey() string {
|
func (p *Plugin) webuiAPIKey(s *sdk.PluginSDK) string {
|
||||||
if cfgReg == nil {
|
if s == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
ps := cfgReg.PluginConfig("webui")
|
v, _ := s.Settings().GetPlugin("webui", "api_key")
|
||||||
if v, _ := ps.Get("api_key"); v != nil {
|
if k, ok := v.(string); ok {
|
||||||
if s, ok := v.(string); ok {
|
return k
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -215,11 +195,11 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo
|
|||||||
case "/help":
|
case "/help":
|
||||||
p.cmdHelp(conn)
|
p.cmdHelp(conn)
|
||||||
case "/status":
|
case "/status":
|
||||||
p.cmdStatus(conn)
|
p.cmdStatus(conn, s)
|
||||||
case "/kernel":
|
case "/kernel":
|
||||||
p.cmdKernel(conn)
|
p.cmdKernel(conn, s)
|
||||||
case "/settings":
|
case "/settings":
|
||||||
p.cmdSettings(conn, parts)
|
p.cmdSettings(conn, parts, s)
|
||||||
case "/plugin":
|
case "/plugin":
|
||||||
p.cmdPlugin(conn, parts, s)
|
p.cmdPlugin(conn, parts, s)
|
||||||
case "/memory":
|
case "/memory":
|
||||||
@ -227,7 +207,7 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo
|
|||||||
case "/knowledge":
|
case "/knowledge":
|
||||||
p.cmdKnowledge(conn, s)
|
p.cmdKnowledge(conn, s)
|
||||||
case "/agents":
|
case "/agents":
|
||||||
p.cmdAgents(conn)
|
p.cmdAgents(conn, s)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -260,12 +240,13 @@ func (p *Plugin) cmdHelp(conn net.Conn) {
|
|||||||
|
|
||||||
// ======== /status ========
|
// ======== /status ========
|
||||||
|
|
||||||
func (p *Plugin) cmdStatus(conn net.Conn) {
|
func (p *Plugin) cmdStatus(conn net.Conn, s *sdk.PluginSDK) {
|
||||||
if statusProv == nil {
|
st := s.Status()
|
||||||
|
if st == nil {
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ks := statusProv.GetKernelStatus()
|
ks := st.GetKernelStatus()
|
||||||
|
|
||||||
llmStatus := "不可用"
|
llmStatus := "不可用"
|
||||||
if ks.LLM.Available {
|
if ks.LLM.Available {
|
||||||
@ -294,19 +275,21 @@ func (p *Plugin) cmdStatus(conn net.Conn) {
|
|||||||
|
|
||||||
// ======== /kernel ========
|
// ======== /kernel ========
|
||||||
|
|
||||||
func (p *Plugin) cmdKernel(conn net.Conn) {
|
func (p *Plugin) cmdKernel(conn net.Conn, s *sdk.PluginSDK) {
|
||||||
if statusProv == nil {
|
st := s.Status()
|
||||||
|
if st == nil {
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, _ := json.MarshalIndent(statusProv.GetKernelStatus(), "", " ")
|
data, _ := json.MarshalIndent(st.GetKernelStatus(), "", " ")
|
||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======== /settings ========
|
// ======== /settings ========
|
||||||
|
|
||||||
func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
|
func (p *Plugin) cmdSettings(conn net.Conn, parts []string, s *sdk.PluginSDK) {
|
||||||
if cfgReg == nil {
|
sett := s.Settings()
|
||||||
|
if sett == nil {
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "config registry not available"})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": "config registry not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -318,7 +301,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
|
|||||||
}
|
}
|
||||||
key := parts[2]
|
key := parts[2]
|
||||||
val := strings.Join(parts[3:], " ")
|
val := strings.Join(parts[3:], " ")
|
||||||
if err := cfgReg.Set(key, val); err != nil {
|
if err := sett.SetCore(key, val); err != nil {
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -330,7 +313,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
|
|||||||
if len(parts) >= 2 {
|
if len(parts) >= 2 {
|
||||||
prefix = parts[1]
|
prefix = parts[1]
|
||||||
}
|
}
|
||||||
keys := cfgReg.List(prefix)
|
keys, _ := sett.ListCore(prefix)
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "无匹配配置项"})
|
writeLine(conn, map[string]interface{}{"type": "response", "content": "无匹配配置项"})
|
||||||
@ -338,7 +321,7 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
|
|||||||
}
|
}
|
||||||
var lines []string
|
var lines []string
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
v, _ := cfgReg.Get(k)
|
v, _ := sett.GetCore(k)
|
||||||
lines = append(lines, fmt.Sprintf(" %s = %v", k, v))
|
lines = append(lines, fmt.Sprintf(" %s = %v", k, v))
|
||||||
}
|
}
|
||||||
writeLine(conn, map[string]interface{}{
|
writeLine(conn, map[string]interface{}{
|
||||||
@ -409,20 +392,22 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) {
|
|||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin remove <name>"})
|
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin remove <name>"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if pmgr == nil {
|
||||||
|
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
name := parts[2]
|
name := parts[2]
|
||||||
if pluginDir == "" {
|
dir := pmgr.PluginDir()
|
||||||
|
if dir == "" {
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin dir not configured"})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin dir not configured"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dir := filepath.Join(pluginDir, name)
|
if err := os.RemoveAll(filepath.Join(dir, name)); err != nil {
|
||||||
if err := os.RemoveAll(dir); err != nil {
|
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 同步清理禁用表
|
// 同步清理禁用表
|
||||||
if pmgr != nil {
|
_ = pmgr.EnablePlugin(name)
|
||||||
pmgr.EnablePlugin(name)
|
|
||||||
}
|
|
||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已删除,执行 /plugin reload 生效", name)})
|
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已删除,执行 /plugin reload 生效", name)})
|
||||||
|
|
||||||
case "info":
|
case "info":
|
||||||
@ -430,20 +415,33 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) {
|
|||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin info <name>"})
|
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin info <name>"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pluginReg == nil {
|
if pmgr == nil {
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
plg := pluginReg.Get(parts[2])
|
name := parts[2]
|
||||||
if plg == nil {
|
metas := pmgr.PluginMetas()
|
||||||
if pmgr != nil && pmgr.IsPluginDisabled(parts[2]) {
|
meta, hasMeta := metas[name]
|
||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 已禁用", parts[2])})
|
loaded := false
|
||||||
return
|
for _, n := range pmgr.ListLoadedPlugins() {
|
||||||
|
if n == name {
|
||||||
|
loaded = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未安装", parts[2])})
|
}
|
||||||
|
if loaded {
|
||||||
|
display := name
|
||||||
|
if hasMeta && meta.NameZh != "" {
|
||||||
|
display = meta.NameZh
|
||||||
|
}
|
||||||
|
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s (%s)\n状态: 已加载", display, name)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s\n状态: 已加载", plg.Name())})
|
if pmgr.IsPluginDisabled(name) {
|
||||||
|
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 已禁用", name)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未安装", name)})
|
||||||
|
|
||||||
case "disable":
|
case "disable":
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
@ -541,12 +539,13 @@ func (p *Plugin) cmdKnowledge(conn net.Conn, s *sdk.PluginSDK) {
|
|||||||
|
|
||||||
// ======== /agents ========
|
// ======== /agents ========
|
||||||
|
|
||||||
func (p *Plugin) cmdAgents(conn net.Conn) {
|
func (p *Plugin) cmdAgents(conn net.Conn, s *sdk.PluginSDK) {
|
||||||
if statusProv == nil {
|
st := s.Status()
|
||||||
|
if st == nil {
|
||||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ks := statusProv.GetKernelStatus()
|
ks := st.GetKernelStatus()
|
||||||
data, _ := json.MarshalIndent(map[string]string{"agent_id": ks.AgentID}, "", " ")
|
data, _ := json.MarshalIndent(map[string]string{"agent_id": ks.AgentID}, "", " ")
|
||||||
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,12 @@ func (tc *toolCapture) RegisterAPI(name string) error { return nil }
|
|||||||
func setupPlugin() (*Plugin, *toolCapture, error) {
|
func setupPlugin() (*Plugin, *toolCapture, error) {
|
||||||
p := New("cmd")
|
p := New("cmd")
|
||||||
tc := newToolCapture()
|
tc := newToolCapture()
|
||||||
sdk := sdk.New("cmd", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
sdk := sdk.New("cmd", sdk.SDKConfig{
|
||||||
|
RegTool: tc.RegisterTool,
|
||||||
|
RegStage: tc.RegisterStage,
|
||||||
|
RegAPI: tc.RegisterAPI,
|
||||||
|
Settings: sdk.NewSettings("cmd", nil),
|
||||||
|
})
|
||||||
if err := p.Start(sdk); err != nil {
|
if err := p.Start(sdk); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,27 +9,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
|
||||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
|
||||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
|
||||||
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
hcStageHost *agentCore.StageHost
|
|
||||||
hcIOMgr *agentIO.IOManager
|
|
||||||
hcPluginReg *plugin.Registry
|
|
||||||
hcMemory *memory.GraphDB
|
|
||||||
hcKnowledge *knowledge.Store
|
|
||||||
hcDocStore *doc.Store
|
|
||||||
hcProviderMgr *agentAPI.ProviderManager
|
|
||||||
hcStatusProvider agentCore.StatusProvider
|
|
||||||
)
|
|
||||||
|
|
||||||
type toolInfo struct {
|
type toolInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
@ -50,24 +33,9 @@ type llmReport struct {
|
|||||||
Detail string `json:"detail,omitempty"`
|
Detail string `json:"detail,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Configure(sh *agentCore.StageHost, iom *agentIO.IOManager, pr *plugin.Registry,
|
|
||||||
mem *memory.GraphDB, ks *knowledge.Store, ds *doc.Store, pm *agentAPI.ProviderManager, sp agentCore.StatusProvider) {
|
|
||||||
hcStageHost = sh
|
|
||||||
hcIOMgr = iom
|
|
||||||
hcPluginReg = pr
|
|
||||||
hcMemory = mem
|
|
||||||
hcKnowledge = ks
|
|
||||||
hcDocStore = ds
|
|
||||||
hcProviderMgr = pm
|
|
||||||
hcStatusProvider = sp
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
plugin.RegisterPluginMeta("healthcheck", "健康检查", "Health Check")
|
plugin.RegisterPluginMeta("healthcheck", "健康检查", "Health Check")
|
||||||
plugin.RegisterFactory("healthcheck", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
plugin.RegisterFactory("healthcheck", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
if hcStageHost == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return New(name), nil
|
return New(name), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -215,7 +183,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
"properties": map[string]interface{}{},
|
"properties": map[string]interface{}{},
|
||||||
},
|
},
|
||||||
}, func(args map[string]interface{}) (interface{}, error) {
|
}, func(args map[string]interface{}) (interface{}, error) {
|
||||||
return p.listAllTools()
|
return p.listAllTools(s)
|
||||||
})
|
})
|
||||||
|
|
||||||
p.selfToolNames["healthcheck_memory"] = true
|
p.selfToolNames["healthcheck_memory"] = true
|
||||||
@ -227,7 +195,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
"properties": map[string]interface{}{},
|
"properties": map[string]interface{}{},
|
||||||
},
|
},
|
||||||
}, func(args map[string]interface{}) (interface{}, error) {
|
}, func(args map[string]interface{}) (interface{}, error) {
|
||||||
return p.checkMemory()
|
return p.checkMemory(s)
|
||||||
})
|
})
|
||||||
|
|
||||||
p.selfToolNames["healthcheck_report"] = true
|
p.selfToolNames["healthcheck_report"] = true
|
||||||
@ -255,7 +223,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
return map[string]interface{}{"ok": true, "received": count}, nil
|
return map[string]interface{}{"ok": true, "received": count}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if hcStatusProvider != nil {
|
if s.Status() != nil {
|
||||||
p.selfToolNames["healthcheck_kernel"] = true
|
p.selfToolNames["healthcheck_kernel"] = true
|
||||||
s.RegisterTool("healthcheck_kernel", sdk.ToolDef{
|
s.RegisterTool("healthcheck_kernel", sdk.ToolDef{
|
||||||
Name: "healthcheck_kernel",
|
Name: "healthcheck_kernel",
|
||||||
@ -265,7 +233,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
"properties": map[string]interface{}{},
|
"properties": map[string]interface{}{},
|
||||||
},
|
},
|
||||||
}, func(args map[string]interface{}) (interface{}, error) {
|
}, func(args map[string]interface{}) (interface{}, error) {
|
||||||
return hcStatusProvider.GetKernelStatus(), nil
|
return s.Status().GetKernelStatus(), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,9 +268,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
p.startAutoCheck(s, p.autoInterval)
|
p.startAutoCheck(s, p.autoInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[healthcheck] ready (stageHost=%v iom=%v reg=%v mem=%v ks=%v ds=%v pm=%v sp=%v)",
|
log.Printf("[healthcheck] ready (tool=%v mem=%v ks=%v ds=%v llm=%v plugins=%v status=%v)",
|
||||||
hcStageHost != nil, hcIOMgr != nil, hcPluginReg != nil,
|
s.Tool() != nil, s.Memory() != nil, s.Knowledge() != nil,
|
||||||
hcMemory != nil, hcKnowledge != nil, hcDocStore != nil, hcProviderMgr != nil, hcStatusProvider != nil)
|
s.DocMemory() != nil, s.LLM() != nil, s.PluginMgr() != nil, s.Status() != nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,36 +335,32 @@ func (p *Plugin) runAutoCheck(s *sdk.PluginSDK) {
|
|||||||
func (p *Plugin) runFullCheck(s *sdk.PluginSDK) (interface{}, error) {
|
func (p *Plugin) runFullCheck(s *sdk.PluginSDK) (interface{}, error) {
|
||||||
results := []checkResult{}
|
results := []checkResult{}
|
||||||
|
|
||||||
pluginResult := p.checkPluginsRaw()
|
pluginResult := p.checkPluginsRaw(s)
|
||||||
results = append(results, pluginResult...)
|
results = append(results, pluginResult...)
|
||||||
|
|
||||||
toolResult := p.checkToolsRaw()
|
toolResult := p.checkToolsRaw(s)
|
||||||
results = append(results, toolResult...)
|
results = append(results, toolResult...)
|
||||||
|
|
||||||
if hcMemory != nil {
|
if s.Memory() != nil {
|
||||||
r := p.testMemoryRaw()
|
results = append(results, p.testMemoryRaw(s))
|
||||||
results = append(results, r)
|
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{Name: "memory", Status: "skip", Detail: "图记忆未初始化", Pass: true})
|
results = append(results, checkResult{Name: "memory", Status: "skip", Detail: "图记忆未初始化", Pass: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hcKnowledge != nil {
|
if s.Knowledge() != nil {
|
||||||
r := p.testKnowledgeRaw()
|
results = append(results, p.testKnowledgeRaw(s))
|
||||||
results = append(results, r)
|
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{Name: "knowledge", Status: "skip", Detail: "知识库未初始化", Pass: true})
|
results = append(results, checkResult{Name: "knowledge", Status: "skip", Detail: "知识库未初始化", Pass: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hcDocStore != nil {
|
if s.DocMemory() != nil {
|
||||||
r := p.testDocStoreRaw()
|
results = append(results, p.testDocStoreRaw(s))
|
||||||
results = append(results, r)
|
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{Name: "documents", Status: "skip", Detail: "文档记忆未初始化", Pass: true})
|
results = append(results, checkResult{Name: "documents", Status: "skip", Detail: "文档记忆未初始化", Pass: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hcProviderMgr != nil {
|
if s.LLM() != nil {
|
||||||
r := p.testLLMDriven()
|
results = append(results, p.testLLMDriven(s))
|
||||||
results = append(results, r)
|
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{Name: "llm_discovery", Status: "skip", Detail: "LLM Provider 未初始化", Pass: true})
|
results = append(results, checkResult{Name: "llm_discovery", Status: "skip", Detail: "LLM Provider 未初始化", Pass: true})
|
||||||
}
|
}
|
||||||
@ -424,7 +388,7 @@ func (p *Plugin) runFullCheck(s *sdk.PluginSDK) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) checkPlugins(s *sdk.PluginSDK) (interface{}, error) {
|
func (p *Plugin) checkPlugins(s *sdk.PluginSDK) (interface{}, error) {
|
||||||
results := p.checkPluginsRaw()
|
results := p.checkPluginsRaw(s)
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"plugins": results,
|
"plugins": results,
|
||||||
@ -432,12 +396,12 @@ func (p *Plugin) checkPlugins(s *sdk.PluginSDK) (interface{}, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) checkPluginsRaw() []checkResult {
|
func (p *Plugin) checkPluginsRaw(s *sdk.PluginSDK) []checkResult {
|
||||||
if hcPluginReg == nil {
|
if s.PluginMgr() == nil {
|
||||||
return []checkResult{{Name: "plugins", Status: "skip", Detail: "插件注册表未初始化", Pass: true}}
|
return []checkResult{{Name: "plugins", Status: "skip", Detail: "插件注册表未初始化", Pass: true}}
|
||||||
}
|
}
|
||||||
|
|
||||||
names := hcPluginReg.List()
|
names := s.PluginMgr().ListLoadedPlugins()
|
||||||
if names == nil {
|
if names == nil {
|
||||||
names = []string{}
|
names = []string{}
|
||||||
}
|
}
|
||||||
@ -449,8 +413,8 @@ func (p *Plugin) checkPluginsRaw() []checkResult {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) listAllTools() (interface{}, error) {
|
func (p *Plugin) listAllTools(s *sdk.PluginSDK) (interface{}, error) {
|
||||||
tools := p.collectAllTools()
|
tools := p.collectAllTools(s)
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"count": len(tools),
|
"count": len(tools),
|
||||||
@ -458,8 +422,8 @@ func (p *Plugin) listAllTools() (interface{}, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) checkToolsRaw() []checkResult {
|
func (p *Plugin) checkToolsRaw(s *sdk.PluginSDK) []checkResult {
|
||||||
tools := p.collectAllTools()
|
tools := p.collectAllTools(s)
|
||||||
return []checkResult{{
|
return []checkResult{{
|
||||||
Name: "tools",
|
Name: "tools",
|
||||||
Status: "ok",
|
Status: "ok",
|
||||||
@ -468,7 +432,7 @@ func (p *Plugin) checkToolsRaw() []checkResult {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) collectAllTools() []toolInfo {
|
func (p *Plugin) collectAllTools(s *sdk.PluginSDK) []toolInfo {
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
var tools []toolInfo
|
var tools []toolInfo
|
||||||
|
|
||||||
@ -480,14 +444,12 @@ func (p *Plugin) collectAllTools() []toolInfo {
|
|||||||
tools = append(tools, toolInfo{Name: name, Source: source, Description: desc})
|
tools = append(tools, toolInfo{Name: name, Source: source, Description: desc})
|
||||||
}
|
}
|
||||||
|
|
||||||
if hcStageHost != nil {
|
if s.Tool() != nil {
|
||||||
for _, def := range hcStageHost.GetToolDefs() {
|
for _, def := range s.Tool().GetToolDefs() {
|
||||||
addTool(def.Name, "plugin", def.Description)
|
addTool(def.Name, "plugin", def.Description)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if hcIOMgr != nil {
|
for _, def := range s.Tool().GetAllTools() {
|
||||||
for _, def := range hcIOMgr.GetAllTools() {
|
|
||||||
addTool(def.Name, "device", def.Description)
|
addTool(def.Name, "device", def.Description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -495,23 +457,18 @@ func (p *Plugin) collectAllTools() []toolInfo {
|
|||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) testMemoryRaw() checkResult {
|
func (p *Plugin) testMemoryRaw(s *sdk.PluginSDK) checkResult {
|
||||||
marker := fmt.Sprintf("_hc_%d", time.Now().UnixNano())
|
marker := fmt.Sprintf("_hc_%d", time.Now().UnixNano())
|
||||||
triples := []memory.Triple{
|
triples := []sdk.Triple{
|
||||||
{Subject: marker, Relation: "is", Object: "healthcheck_test", SubjectType: "System", ObjectType: "Flag"},
|
{Subject: marker, Relation: "is", Object: "healthcheck_test", SubjectType: "System", ObjectType: "Flag"},
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
ec, rc, err := hcMemory.Commit(triples, "healthcheck", 0)
|
if err := s.Memory().Commit(triples); err != nil {
|
||||||
if err != nil {
|
|
||||||
return checkResult{Name: "memory_write", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
|
return checkResult{Name: "memory_write", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err := hcMemory.Commit(triples, "healthcheck_cleanup", 0); err != nil {
|
n, err := s.Memory().Purge(map[string]string{"subject_contains": marker}, "hard")
|
||||||
log.Printf("[healthcheck] memory cleanup error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := hcMemory.Purge(map[string]string{"subject_contains": marker}, "hard")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return checkResult{Name: "memory_purge", Status: "fail", Detail: fmt.Sprintf("清理失败: %v", err), Pass: false}
|
return checkResult{Name: "memory_purge", Status: "fail", Detail: fmt.Sprintf("清理失败: %v", err), Pass: false}
|
||||||
}
|
}
|
||||||
@ -520,25 +477,29 @@ func (p *Plugin) testMemoryRaw() checkResult {
|
|||||||
return checkResult{
|
return checkResult{
|
||||||
Name: "memory",
|
Name: "memory",
|
||||||
Status: "ok",
|
Status: "ok",
|
||||||
Detail: fmt.Sprintf("写入 %d 实体/%d 关系, 清理 %d 条, 耗时 %v", ec, rc, n, elapsed.Round(time.Millisecond)),
|
Detail: fmt.Sprintf("写入+清理 %d 条, 耗时 %v", n, elapsed.Round(time.Millisecond)),
|
||||||
Pass: true,
|
Pass: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) testKnowledgeRaw() checkResult {
|
func (p *Plugin) testKnowledgeRaw(s *sdk.PluginSDK) checkResult {
|
||||||
marker := fmt.Sprintf("_hc_knowledge_test_%d", time.Now().UnixNano())
|
marker := fmt.Sprintf("_hc_knowledge_test_%d", time.Now().UnixNano())
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
if err := hcKnowledge.Add(marker, "健康检查测试标记,可忽略"); err != nil {
|
if err := s.Knowledge().Add(marker, "健康检查测试标记,可忽略"); err != nil {
|
||||||
return checkResult{Name: "knowledge", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
|
return checkResult{Name: "knowledge", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
results := hcKnowledge.Search("健康检查测试标记", 3)
|
results, err := s.Knowledge().Search("健康检查测试标记", 3)
|
||||||
|
if err != nil {
|
||||||
|
s.Knowledge().Remove(marker)
|
||||||
|
return checkResult{Name: "knowledge", Status: "fail", Detail: fmt.Sprintf("查询失败: %v", err), Pass: false}
|
||||||
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
// 清理测试条目,避免积累
|
// 清理测试条目,避免积累
|
||||||
hcKnowledge.Remove(marker)
|
s.Knowledge().Remove(marker)
|
||||||
|
|
||||||
if len(results) > 0 {
|
if len(results) > 0 {
|
||||||
return checkResult{
|
return checkResult{
|
||||||
@ -557,20 +518,21 @@ func (p *Plugin) testKnowledgeRaw() checkResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) testDocStoreRaw() checkResult {
|
func (p *Plugin) testDocStoreRaw(s *sdk.PluginSDK) checkResult {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
doc := &doc.Doc{
|
doc := &sdk.Doc{
|
||||||
Summary: "健康检查测试文档",
|
Title: fmt.Sprintf("健康检查测试文档 %d", time.Now().UnixNano()),
|
||||||
Content: "这是一条由 healthcheck 插件创建的测试文档,用于验证文档记忆系统是否正常工作。",
|
Content: "这是一条由 healthcheck 插件创建的测试文档,用于验证文档记忆系统是否正常工作。",
|
||||||
Tags: []string{"healthcheck", "test"},
|
|
||||||
Source: "healthcheck",
|
|
||||||
}
|
}
|
||||||
if err := hcDocStore.Insert(doc); err != nil {
|
if err := s.DocMemory().Insert(doc); err != nil {
|
||||||
return checkResult{Name: "documents", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
|
return checkResult{Name: "documents", Status: "fail", Detail: fmt.Sprintf("写入失败: %v", err), Pass: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.ID != "" {
|
// 清理测试文档,避免积累(SDK Insert 不回填 ID,经 Query 按标题定位)
|
||||||
hcDocStore.Remove(doc.ID)
|
for _, d := range s.DocMemory().Query("健康检查测试文档", 10) {
|
||||||
|
if d.ID != "" && strings.HasPrefix(d.Title, "健康检查测试文档") {
|
||||||
|
s.DocMemory().Remove(d.ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
@ -582,9 +544,9 @@ func (p *Plugin) testDocStoreRaw() checkResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) testLLMDriven() checkResult {
|
func (p *Plugin) testLLMDriven(s *sdk.PluginSDK) checkResult {
|
||||||
provider := hcProviderMgr.Default()
|
llmName := s.LLM().CurrentSource()
|
||||||
if provider == nil {
|
if llmName == "" {
|
||||||
return checkResult{Name: "llm_discovery", Status: "skip", Detail: "无可用 LLM Provider", Pass: true}
|
return checkResult{Name: "llm_discovery", Status: "skip", Detail: "无可用 LLM Provider", Pass: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,7 +555,7 @@ func (p *Plugin) testLLMDriven() checkResult {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// 收集所有工具定义(排除健康检查自身的工具以避免循环测试)
|
// 收集所有工具定义(排除健康检查自身的工具以避免循环测试)
|
||||||
toolDefs := p.collectToolDefsForLLM()
|
toolDefs := p.collectToolDefsForLLM(s)
|
||||||
|
|
||||||
if len(toolDefs) == 0 {
|
if len(toolDefs) == 0 {
|
||||||
return checkResult{Name: "llm_discovery", Status: "skip", Detail: "没有可测试的工具", Pass: true}
|
return checkResult{Name: "llm_discovery", Status: "skip", Detail: "没有可测试的工具", Pass: true}
|
||||||
@ -608,15 +570,14 @@ func (p *Plugin) testLLMDriven() checkResult {
|
|||||||
// 构建 prompt
|
// 构建 prompt
|
||||||
prompt := p.buildDiscoveryPrompt(toolDefs)
|
prompt := p.buildDiscoveryPrompt(toolDefs)
|
||||||
|
|
||||||
msgs := []agentAPI.Message{{Role: "user", Content: prompt}}
|
msgs := []sdk.LLMMessage{{Role: "user", Content: prompt}}
|
||||||
tools := convertToolDefs(toolDefs)
|
tools := convertToolDefs(toolDefs)
|
||||||
|
|
||||||
llmName := provider.Name()
|
|
||||||
turnCount := 0
|
turnCount := 0
|
||||||
toolCallCount := 0
|
toolCallCount := 0
|
||||||
|
|
||||||
for turn := 0; turn < p.llmMaxTurns; turn++ {
|
for turn := 0; turn < p.llmMaxTurns; turn++ {
|
||||||
resp, err := provider.Chat(ctx, &agentAPI.CompletionRequest{
|
resp, err := s.LLM().Chat(ctx, &sdk.LLMCompletionRequest{
|
||||||
Messages: msgs,
|
Messages: msgs,
|
||||||
MaxTokens: p.llmMaxTokens,
|
MaxTokens: p.llmMaxTokens,
|
||||||
Tools: tools,
|
Tools: tools,
|
||||||
@ -638,12 +599,12 @@ func (p *Plugin) testLLMDriven() checkResult {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls})
|
msgs = append(msgs, sdk.LLMMessage{Role: "assistant", Content: resp.Content, ToolCalls: resp.ToolCalls})
|
||||||
|
|
||||||
for _, tc := range resp.ToolCalls {
|
for _, tc := range resp.ToolCalls {
|
||||||
toolCallCount++
|
toolCallCount++
|
||||||
content := p.executeToolForLLM(tc)
|
content := p.executeToolForLLM(s, tc)
|
||||||
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: content})
|
msgs = append(msgs, sdk.LLMMessage{Role: "tool", ToolCallID: tc.ID, Content: content})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,7 +627,7 @@ func (p *Plugin) testLLMDriven() checkResult {
|
|||||||
|
|
||||||
// collectToolDefsForLLM 收集全部已注册的工具定义供 LLM 发现和测试。
|
// collectToolDefsForLLM 收集全部已注册的工具定义供 LLM 发现和测试。
|
||||||
// 动态排除本插件自身注册的工具(通过 selfToolNames),避免 LLM 自我循环调用。
|
// 动态排除本插件自身注册的工具(通过 selfToolNames),避免 LLM 自我循环调用。
|
||||||
func (p *Plugin) collectToolDefsForLLM() []sdk.ToolDef {
|
func (p *Plugin) collectToolDefsForLLM(s *sdk.PluginSDK) []sdk.ToolDef {
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
var defs []sdk.ToolDef
|
var defs []sdk.ToolDef
|
||||||
|
|
||||||
@ -678,14 +639,12 @@ func (p *Plugin) collectToolDefsForLLM() []sdk.ToolDef {
|
|||||||
defs = append(defs, d)
|
defs = append(defs, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hcStageHost != nil {
|
if s.Tool() != nil {
|
||||||
for _, d := range hcStageHost.GetToolDefs() {
|
for _, d := range s.Tool().GetToolDefs() {
|
||||||
addDef(d)
|
addDef(d)
|
||||||
}
|
}
|
||||||
}
|
for _, d := range s.Tool().GetAllTools() {
|
||||||
if hcIOMgr != nil {
|
addDef(d)
|
||||||
for _, d := range hcIOMgr.GetAllTools() {
|
|
||||||
addDef(sdk.ToolDef{Name: d.Name, Description: d.Description, Parameters: d.Parameters})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -715,10 +674,10 @@ func (p *Plugin) buildDiscoveryPrompt(toolDefs []sdk.ToolDef) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// executeToolForLLM 在 LLM 工具循环中执行工具调用。
|
// executeToolForLLM 在 LLM 工具循环中执行工具调用。
|
||||||
// healthcheck_report 通过 StageHost 路由到自身注册的 handler,负责收集 LLM 上报。
|
// healthcheck_report 经 SDK ToolAPI 路由到自身注册的 handler,负责收集 LLM 上报。
|
||||||
func (p *Plugin) executeToolForLLM(tc agentAPI.ToolCall) string {
|
func (p *Plugin) executeToolForLLM(s *sdk.PluginSDK, tc sdk.LLMToolCall) string {
|
||||||
if hcStageHost != nil {
|
if s.Tool() != nil {
|
||||||
result, err := hcStageHost.ExecuteTool(tc.Name, tc.Arguments)
|
result, err := s.Tool().ExecuteTool(tc.Name, tc.Arguments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("调用工具 %s 失败: %v", tc.Name, err)
|
return fmt.Sprintf("调用工具 %s 失败: %v", tc.Name, err)
|
||||||
}
|
}
|
||||||
@ -726,7 +685,7 @@ func (p *Plugin) executeToolForLLM(tc agentAPI.ToolCall) string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("工具 %s 不可执行(StageHost 未初始化)", tc.Name)
|
return fmt.Sprintf("工具 %s 不可执行(工具注册表未初始化)", tc.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToolDefs(defs []sdk.ToolDef) []interface{} {
|
func convertToolDefs(defs []sdk.ToolDef) []interface{} {
|
||||||
@ -744,11 +703,11 @@ func convertToolDefs(defs []sdk.ToolDef) []interface{} {
|
|||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) checkMemory() (interface{}, error) {
|
func (p *Plugin) checkMemory(s *sdk.PluginSDK) (interface{}, error) {
|
||||||
if hcMemory == nil {
|
if s.Memory() == nil {
|
||||||
return map[string]interface{}{"status": "skip", "pass": true, "detail": "图记忆未初始化"}, nil
|
return map[string]interface{}{"status": "skip", "pass": true, "detail": "图记忆未初始化"}, nil
|
||||||
}
|
}
|
||||||
r := p.testMemoryRaw()
|
r := p.testMemoryRaw(s)
|
||||||
c := map[string]interface{}{
|
c := map[string]interface{}{
|
||||||
"status": r.Status,
|
"status": r.Status,
|
||||||
"pass": r.Pass,
|
"pass": r.Pass,
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||||
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,16 +33,28 @@ func (tc *toolCapture) RegisterTool(name string, def sdk.ToolDef, handler sdk.To
|
|||||||
func (tc *toolCapture) RegisterStage(stage sdk.Stage, handler sdk.StageHandler) {}
|
func (tc *toolCapture) RegisterStage(stage sdk.Stage, handler sdk.StageHandler) {}
|
||||||
func (tc *toolCapture) RegisterAPI(name string) error { return nil }
|
func (tc *toolCapture) RegisterAPI(name string) error { return nil }
|
||||||
|
|
||||||
func setupPlugin() (*Plugin, *toolCapture, error) {
|
func newTestSDK(cfg sdk.SDKConfig) *sdk.PluginSDK {
|
||||||
sh := agentCore.NewStageHost()
|
if cfg.Settings == nil {
|
||||||
iom := agentIO.NewIOManager()
|
cfg.Settings = sdk.NewSettings("healthcheck", nil)
|
||||||
pr := plugin.NewRegistry()
|
}
|
||||||
|
if cfg.Tool == nil {
|
||||||
|
cfg.Tool = sdk.NewTool(agentCore.NewStageHost(), agentIO.NewIOManager())
|
||||||
|
}
|
||||||
|
return sdk.New("healthcheck", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
Configure(sh, iom, pr, nil, nil, nil, nil, nil)
|
func setupPlugin() (*Plugin, *toolCapture, error) {
|
||||||
p := New("healthcheck")
|
return setupPluginWith(sdk.SDKConfig{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupPluginWith(cfg sdk.SDKConfig) (*Plugin, *toolCapture, error) {
|
||||||
tc := newToolCapture()
|
tc := newToolCapture()
|
||||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
cfg.RegTool = tc.RegisterTool
|
||||||
if err := p.Start(sdk); err != nil {
|
cfg.RegStage = tc.RegisterStage
|
||||||
|
cfg.RegAPI = tc.RegisterAPI
|
||||||
|
p := New("healthcheck")
|
||||||
|
s := newTestSDK(cfg)
|
||||||
|
if err := p.Start(s); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return p, tc, nil
|
return p, tc, nil
|
||||||
@ -185,15 +196,8 @@ func TestHealthcheckWithMemory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer memDB.Close()
|
defer memDB.Close()
|
||||||
|
|
||||||
sh := agentCore.NewStageHost()
|
_, tc, err := setupPluginWith(sdk.SDKConfig{Memory: sdk.NewGraphMemory(memDB)})
|
||||||
iom := agentIO.NewIOManager()
|
if err != nil {
|
||||||
pr := plugin.NewRegistry()
|
|
||||||
|
|
||||||
Configure(sh, iom, pr, memDB, nil, nil, nil, nil)
|
|
||||||
p := New("healthcheck")
|
|
||||||
tc := newToolCapture()
|
|
||||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
|
||||||
if err := p.Start(sdk); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,15 +232,8 @@ func TestHealthcheckWithKnowledge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer ks.Stop()
|
defer ks.Stop()
|
||||||
|
|
||||||
sh := agentCore.NewStageHost()
|
_, tc, err := setupPluginWith(sdk.SDKConfig{Knowledge: sdk.NewKnowledge(ks)})
|
||||||
iom := agentIO.NewIOManager()
|
if err != nil {
|
||||||
pr := plugin.NewRegistry()
|
|
||||||
|
|
||||||
Configure(sh, iom, pr, nil, ks, nil, nil, nil)
|
|
||||||
p := New("healthcheck")
|
|
||||||
tc := newToolCapture()
|
|
||||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
|
||||||
if err := p.Start(sdk); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,15 +279,8 @@ func TestHealthcheckWithDocStore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer ds.Stop()
|
defer ds.Stop()
|
||||||
|
|
||||||
sh := agentCore.NewStageHost()
|
_, tc, err := setupPluginWith(sdk.SDKConfig{DocMemory: sdk.NewDocMemory(ds)})
|
||||||
iom := agentIO.NewIOManager()
|
if err != nil {
|
||||||
pr := plugin.NewRegistry()
|
|
||||||
|
|
||||||
Configure(sh, iom, pr, nil, nil, ds, nil, nil)
|
|
||||||
p := New("healthcheck")
|
|
||||||
tc := newToolCapture()
|
|
||||||
sdk := sdk.New("healthcheck", sdk.SDKConfig{RegTool: tc.RegisterTool, RegStage: tc.RegisterStage, RegAPI: tc.RegisterAPI})
|
|
||||||
if err := p.Start(sdk); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,11 +330,4 @@ func TestLLMReportCollection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigureNilStageHost(t *testing.T) {
|
|
||||||
Configure(nil, nil, nil, nil, nil, nil, nil, nil)
|
|
||||||
if hcStageHost != nil {
|
|
||||||
t.Fatal("expected hcStageHost to be nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -10,15 +10,13 @@ import (
|
|||||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
||||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||||
|
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||||
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
||||||
healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
|
||||||
openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter"
|
|
||||||
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,11 +75,17 @@ func setupIntegrationWithProvider(t *testing.T, pm *agentAPI.ProviderManager) *t
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluginReg.SetStageHost(stageHost)
|
||||||
|
pluginReg.SetProviderManager(pm)
|
||||||
|
pluginReg.SetKnowledge(ks)
|
||||||
|
pluginReg.SetDocStore(docStore)
|
||||||
|
|
||||||
cli.DefaultSocket = filepath.Join(tmpDir, "cli.sock")
|
cli.DefaultSocket = filepath.Join(tmpDir, "cli.sock")
|
||||||
openclaw.SkillsDir = filepath.Join(tmpDir, "skills")
|
|
||||||
os.MkdirAll(openclaw.SkillsDir, 0755)
|
// 经 ConfigRegistry 装配内核路径配置(clawhubadapter/pluginmgr 等经 SDK settings 读取)
|
||||||
webui.Configure(":0", nil, memDB, nil, nil, nil, iom, nil, ks, nil, nil, pluginReg, nil, nil)
|
cfgReg := internalConfig.NewConfigRegistry("")
|
||||||
healthcheck.Configure(stageHost, iom, pluginReg, memDB, ks, docStore, pm, nil)
|
cfgReg.SeedDefaults(tmpDir)
|
||||||
|
pluginReg.SetConfigRegistry(cfgReg)
|
||||||
|
|
||||||
plgDir := filepath.Join(tmpDir, "plugins")
|
plgDir := filepath.Join(tmpDir, "plugins")
|
||||||
os.MkdirAll(plgDir, 0755)
|
os.MkdirAll(plgDir, 0755)
|
||||||
@ -229,7 +233,7 @@ func TestIntegrationCmdRunStderr(t *testing.T) {
|
|||||||
defer env.cleanup()
|
defer env.cleanup()
|
||||||
|
|
||||||
result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
|
result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
|
||||||
"command": "echo stderr_test >&2",
|
"command": "sh -c \"echo stderr_test >&2\"",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -520,7 +524,7 @@ func TestIntegrationLLMDrivenDiscoveryWithRealKey(t *testing.T) {
|
|||||||
Model: "deepseek-v4-flash",
|
Model: "deepseek-v4-flash",
|
||||||
BaseURL: "https://api.deepseek.com",
|
BaseURL: "https://api.deepseek.com",
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
}, vm, "deepseek"))
|
}, vm, "deepseek", "deepseek"))
|
||||||
|
|
||||||
// Setup — 加载所有真实内置插件
|
// Setup — 加载所有真实内置插件
|
||||||
env := setupIntegrationWithProvider(t, pm)
|
env := setupIntegrationWithProvider(t, pm)
|
||||||
|
|||||||
@ -66,11 +66,7 @@ var downloadClient = &http.Client{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var HTTPAddr = "127.0.0.1:9876" // 监听地址,可被 settings 配置
|
||||||
PluginDir string // 由 main.go 设置
|
|
||||||
Reg *plugin.Registry // 由 main.go 设置
|
|
||||||
HTTPAddr = "127.0.0.1:9876" // 监听地址,可被 main.go 覆写或 settings 配置
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
plugin.RegisterPluginMeta("pluginmgr", "插件管理", "Plugin Manager")
|
plugin.RegisterPluginMeta("pluginmgr", "插件管理", "Plugin Manager")
|
||||||
@ -80,12 +76,14 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
name string
|
name string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
server *http.Server
|
server *http.Server
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
listen net.Listener
|
listen net.Listener
|
||||||
httpURL string
|
httpURL string
|
||||||
|
sdk *sdk.PluginSDK
|
||||||
|
pluginDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(name string) *Plugin {
|
func New(name string) *Plugin {
|
||||||
@ -96,6 +94,7 @@ func (p *Plugin) Name() string { return p.name }
|
|||||||
|
|
||||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||||
s.SetAutoRestart(true)
|
s.SetAutoRestart(true)
|
||||||
|
p.sdk = s
|
||||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||||
Key: "http_addr",
|
Key: "http_addr",
|
||||||
Default: HTTPAddr,
|
Default: HTTPAddr,
|
||||||
@ -111,6 +110,12 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, _ := s.Settings().GetCore("plugin.dir"); v != nil {
|
||||||
|
if dir, ok := v.(string); ok && dir != "" {
|
||||||
|
p.pluginDir = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.registerTools(s)
|
p.registerTools(s)
|
||||||
|
|
||||||
if HTTPAddr != "" {
|
if HTTPAddr != "" {
|
||||||
@ -374,7 +379,7 @@ func (p *Plugin) installFromData(data []byte) (interface{}, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := PluginDir
|
dir := p.pluginDir
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return map[string]interface{}{"error": "plugin dir not configured"}, nil
|
return map[string]interface{}{"error": "plugin dir not configured"}, nil
|
||||||
}
|
}
|
||||||
@ -409,7 +414,7 @@ func (p *Plugin) installFromData(data []byte) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) listPlugins() (interface{}, error) {
|
func (p *Plugin) listPlugins() (interface{}, error) {
|
||||||
dir := PluginDir
|
dir := p.pluginDir
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return []map[string]interface{}{}, nil
|
return []map[string]interface{}{}, nil
|
||||||
}
|
}
|
||||||
@ -446,7 +451,7 @@ func (p *Plugin) listPlugins() (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) removePlugin(name string) (interface{}, error) {
|
func (p *Plugin) removePlugin(name string) (interface{}, error) {
|
||||||
dir := filepath.Join(PluginDir, name)
|
dir := filepath.Join(p.pluginDir, name)
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
return map[string]interface{}{"error": "plugin not found", "name": name}, nil
|
return map[string]interface{}{"error": "plugin not found", "name": name}, nil
|
||||||
}
|
}
|
||||||
@ -456,8 +461,10 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 同步清理禁用表
|
// 同步清理禁用表
|
||||||
if Reg != nil {
|
if p.sdk != nil && p.sdk.PluginMgr() != nil {
|
||||||
Reg.EnablePlugin(name)
|
if err := p.sdk.PluginMgr().EnablePlugin(name); err != nil {
|
||||||
|
log.Printf("[pluginmgr] enable %s after remove: %v", name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
@ -468,7 +475,7 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) pluginInfo(name string) (interface{}, error) {
|
func (p *Plugin) pluginInfo(name string) (interface{}, error) {
|
||||||
dir := filepath.Join(PluginDir, name)
|
dir := filepath.Join(p.pluginDir, name)
|
||||||
m, err := plugin.ReadManifest(dir)
|
m, err := plugin.ReadManifest(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("plugin %q not found", name)
|
return nil, fmt.Errorf("plugin %q not found", name)
|
||||||
|
|||||||
@ -9,28 +9,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
|
||||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
|
||||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
|
||||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
|
|
||||||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,26 +34,24 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
supervisor *supervisor.Daemon
|
sdk *sdk.PluginSDK
|
||||||
memory *memory.GraphDB
|
supervisor sdk.SupervisorAPI
|
||||||
indexer *memory.Indexer
|
memory sdk.MemoryAPI
|
||||||
skills *skill.Manager
|
indexer sdk.IndexerAPI
|
||||||
lua *luaVM.VM
|
skills sdk.SkillAPI
|
||||||
config *types.Config
|
adapter sdk.AdapterAPI
|
||||||
startTime time.Time
|
config sdk.ConfigAPI
|
||||||
iom *agentIO.IOManager
|
startTime time.Time
|
||||||
textMem *text.Memory
|
textMem sdk.TextMemoryAPI
|
||||||
knowledge *knowledge.Store
|
knowledge sdk.KnowledgeAPI
|
||||||
tracker *tracker.Tracker
|
tracker sdk.TrackerAPI
|
||||||
cfgReg *internalConfig.ConfigRegistry
|
settings sdk.SettingsAPI
|
||||||
pluginReg *plugin.Registry
|
pluginMgr sdk.PluginManager
|
||||||
pluginMgr sdk.PluginManager
|
status sdk.StatusAPI
|
||||||
eventBus *events.Bus
|
llm sdk.LLMAPI
|
||||||
statusProvider agentCore.StatusProvider
|
|
||||||
providerMgr *agentAPI.ProviderManager
|
sessionMu sync.Mutex
|
||||||
baseAPIKey string
|
sessions map[string]time.Time
|
||||||
sessionMu sync.Mutex
|
|
||||||
sessions map[string]time.Time
|
|
||||||
|
|
||||||
chatMu sync.Mutex
|
chatMu sync.Mutex
|
||||||
chatHistory []ChatMsg
|
chatHistory []ChatMsg
|
||||||
@ -104,51 +87,64 @@ type termState struct {
|
|||||||
created time.Time
|
created time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) SetPluginMgr(mgr sdk.PluginManager) { h.pluginMgr = mgr }
|
|
||||||
|
|
||||||
const maxChatHistory = 200
|
const maxChatHistory = 200
|
||||||
const maxCmdHistory = 100
|
const maxCmdHistory = 100
|
||||||
const maxTerminals = 50
|
const maxTerminals = 50
|
||||||
|
|
||||||
func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager, lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager, tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker, cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus, sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string) *Handler {
|
func NewHandler(s *sdk.PluginSDK) *Handler {
|
||||||
var idx *memory.Indexer
|
var (
|
||||||
if mem != nil {
|
sup sdk.SupervisorAPI
|
||||||
idx = memory.NewIndexer(mem)
|
mem sdk.MemoryAPI
|
||||||
|
idx sdk.IndexerAPI
|
||||||
|
sk sdk.SkillAPI
|
||||||
|
ad sdk.AdapterAPI
|
||||||
|
cfg sdk.ConfigAPI
|
||||||
|
tm sdk.TextMemoryAPI
|
||||||
|
ks sdk.KnowledgeAPI
|
||||||
|
tr sdk.TrackerAPI
|
||||||
|
se sdk.SettingsAPI
|
||||||
|
pm sdk.PluginManager
|
||||||
|
st sdk.StatusAPI
|
||||||
|
llm sdk.LLMAPI
|
||||||
|
)
|
||||||
|
if s != nil {
|
||||||
|
sup, mem, idx = s.Supervisor(), s.Memory(), s.Indexer()
|
||||||
|
sk, ad, cfg = s.Skill(), s.Adapter(), s.Config()
|
||||||
|
tm, ks, tr = s.TextMemory(), s.Knowledge(), s.Tracker()
|
||||||
|
se, pm = s.Settings(), s.PluginMgr()
|
||||||
|
st, llm = s.Status(), s.LLM()
|
||||||
}
|
}
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
supervisor: sup,
|
sdk: s,
|
||||||
memory: mem,
|
supervisor: sup,
|
||||||
indexer: idx,
|
memory: mem,
|
||||||
skills: sk,
|
indexer: idx,
|
||||||
lua: lua,
|
skills: sk,
|
||||||
config: cfg,
|
adapter: ad,
|
||||||
startTime: time.Now(),
|
config: cfg,
|
||||||
iom: iom,
|
startTime: time.Now(),
|
||||||
textMem: tm,
|
textMem: tm,
|
||||||
knowledge: ks,
|
knowledge: ks,
|
||||||
tracker: tr,
|
tracker: tr,
|
||||||
cfgReg: cr,
|
settings: se,
|
||||||
pluginReg: pr,
|
pluginMgr: pm,
|
||||||
eventBus: evBus,
|
status: st,
|
||||||
statusProvider: sp,
|
llm: llm,
|
||||||
providerMgr: pm,
|
sessions: make(map[string]time.Time),
|
||||||
baseAPIKey: baseKey,
|
termStates: make(map[string]*termState),
|
||||||
sessions: make(map[string]time.Time),
|
|
||||||
termStates: make(map[string]*termState),
|
|
||||||
}
|
}
|
||||||
h.loadChatHistory()
|
h.loadChatHistory()
|
||||||
if evBus != nil {
|
if s != nil {
|
||||||
go h.trackToolEvents()
|
go h.trackToolEvents()
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) loadChatHistory() {
|
func (h *Handler) loadChatHistory() {
|
||||||
if h.cfgReg == nil {
|
if h.settings == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ps := h.cfgReg.PluginConfig("webui")
|
v, err := h.settings.Get("chathistory")
|
||||||
v, err := ps.Get("chathistory")
|
|
||||||
if err != nil || v == nil {
|
if err != nil || v == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -166,12 +162,15 @@ func (h *Handler) loadChatHistory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) trackToolEvents() {
|
func (h *Handler) trackToolEvents() {
|
||||||
h.eventBus.Subscribe(events.EventToolCall, func(ev *events.Event) {
|
if h.sdk == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.sdk.Subscribe(sdk.EventToolCall, func(ev *sdk.Event) {
|
||||||
h.handleToolEvent(ev)
|
h.handleToolEvent(ev)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleToolEvent(ev *events.Event) {
|
func (h *Handler) handleToolEvent(ev *sdk.Event) {
|
||||||
payload := ev.Payload
|
payload := ev.Payload
|
||||||
tool, _ := payload["tool"].(string)
|
tool, _ := payload["tool"].(string)
|
||||||
args, _ := payload["args"].(map[string]interface{})
|
args, _ := payload["args"].(map[string]interface{})
|
||||||
@ -235,20 +234,19 @@ func getStr(m map[string]interface{}, key string) string {
|
|||||||
|
|
||||||
func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.Duration) {
|
func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.Duration) {
|
||||||
ttl = 24 * time.Hour
|
ttl = 24 * time.Hour
|
||||||
if h.cfgReg == nil {
|
if h.settings == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ps := h.cfgReg.PluginConfig("webui")
|
if v, _ := h.settings.Get("api_key"); v != nil {
|
||||||
if v, _ := ps.Get("api_key"); v != nil {
|
|
||||||
apiKey, _ = v.(string)
|
apiKey, _ = v.(string)
|
||||||
}
|
}
|
||||||
if v, _ := ps.Get("username"); v != nil {
|
if v, _ := h.settings.Get("username"); v != nil {
|
||||||
username, _ = v.(string)
|
username, _ = v.(string)
|
||||||
}
|
}
|
||||||
if v, _ := ps.Get("password"); v != nil {
|
if v, _ := h.settings.Get("password"); v != nil {
|
||||||
password, _ = v.(string)
|
password, _ = v.(string)
|
||||||
}
|
}
|
||||||
if v, _ := ps.Get("session_ttl_hours"); v != nil {
|
if v, _ := h.settings.Get("session_ttl_hours"); v != nil {
|
||||||
switch n := v.(type) {
|
switch n := v.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
if n > 0 { ttl = time.Duration(n) * time.Hour }
|
if n > 0 { ttl = time.Duration(n) * time.Hour }
|
||||||
@ -433,12 +431,15 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
agents := h.supervisor.ListAgents()
|
agentCount := 0
|
||||||
|
if h.supervisor != nil {
|
||||||
|
agentCount = len(h.supervisor.ListAgents())
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"uptime": time.Since(h.startTime).Round(time.Second).String(),
|
"uptime": time.Since(h.startTime).Round(time.Second).String(),
|
||||||
"agents": len(agents),
|
"agents": agentCount,
|
||||||
"version": meta.Version,
|
"version": sdk.SDKVersion,
|
||||||
"startedAt": h.startTime,
|
"startedAt": h.startTime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -448,16 +449,20 @@ func (h *Handler) handleKernel(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.statusProvider == nil {
|
if h.status == nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "kernel status provider not available"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "kernel status provider not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, h.statusProvider.GetKernelStatus())
|
writeJSON(w, http.StatusOK, h.status.GetKernelStatus())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
if h.supervisor == nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
agents := h.supervisor.ListAgents()
|
agents := h.supervisor.ListAgents()
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"agents": agents})
|
writeJSON(w, http.StatusOK, map[string]interface{}{"agents": agents})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
@ -470,7 +475,10 @@ func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "agent id is required"})
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "agent id is required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.config.Agents = append(h.config.Agents, cfg)
|
if h.config != nil {
|
||||||
|
kcfg := h.config.Get()
|
||||||
|
kcfg.Agents = append(kcfg.Agents, cfg)
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusCreated, map[string]string{"id": string(cfg.ID)})
|
writeJSON(w, http.StatusCreated, map[string]string{"id": string(cfg.ID)})
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
@ -484,7 +492,11 @@ func (h *Handler) handleAgentByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
status, err := h.supervisor.GetAgentStatus(agentID)
|
if h.supervisor == nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err := h.supervisor.GetAgentStatus(string(agentID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@ -515,7 +527,11 @@ func (h *Handler) handleSnapshots(w http.ResponseWriter, r *http.Request, agentI
|
|||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"agent_id": agentID, "snapshots": []map[string]interface{}{}})
|
writeJSON(w, http.StatusOK, map[string]interface{}{"agent_id": agentID, "snapshots": []map[string]interface{}{}})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
snap, err := h.supervisor.PreActionSnapshot(agentID)
|
if h.supervisor == nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
snap, err := h.supervisor.PreActionSnapshot(string(agentID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@ -531,8 +547,12 @@ func (h *Handler) handleRollback(w http.ResponseWriter, r *http.Request, agentID
|
|||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if h.supervisor == nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
snapID := types.SnapshotID(parts[2])
|
snapID := types.SnapshotID(parts[2])
|
||||||
if err := h.supervisor.RollbackAgent(agentID, snapID); err != nil {
|
if err := h.supervisor.RollbackAgent(string(agentID), string(snapID)); err != nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -598,28 +618,25 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
|
|||||||
if depth <= 0 {
|
if depth <= 0 {
|
||||||
depth = 2
|
depth = 2
|
||||||
}
|
}
|
||||||
result, err := h.memory.Recall(keywords, nil, depth, "")
|
entities, relations, err := h.memory.Recall(keywords, depth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, result)
|
writeJSON(w, http.StatusOK, map[string]interface{}{"entities": entities, "relations": relations})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
var req struct {
|
var req struct {
|
||||||
Triples []memory.Triple `json:"triples"`
|
Triples []sdk.Triple `json:"triples"`
|
||||||
SessionID string `json:"session_id"`
|
|
||||||
TurnID int `json:"turn_id"`
|
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ec, rc, err := h.memory.Commit(req.Triples, req.SessionID, req.TurnID)
|
if err := h.memory.Commit(req.Triples); err != nil {
|
||||||
if err != nil {
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusCreated, map[string]int{"entities_created": ec, "relations_created": rc})
|
writeJSON(w, http.StatusCreated, map[string]interface{}{"status": "committed", "committed": len(req.Triples)})
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
var req struct {
|
var req struct {
|
||||||
Criteria map[string]string `json:"criteria"`
|
Criteria map[string]string `json:"criteria"`
|
||||||
@ -650,7 +667,11 @@ func (h *Handler) handleMemoryContext(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
userInput := r.URL.Query().Get("q")
|
userInput := r.URL.Query().Get("q")
|
||||||
injected := h.indexer.BuildContext(userInput)
|
injected, err := h.indexer.BuildContext(userInput)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"context": h.indexer.FormatContext(injected),
|
"context": h.indexer.FormatContext(injected),
|
||||||
"summary": injected.Summary,
|
"summary": injected.Summary,
|
||||||
@ -701,12 +722,13 @@ func (h *Handler) handleKnowledge(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
query := r.URL.Query().Get("q")
|
query := r.URL.Query().Get("q")
|
||||||
if query != "" {
|
if query != "" {
|
||||||
results := h.knowledge.Search(query, 10)
|
results, _ := h.knowledge.Search(query, 10)
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
|
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
categories, _ := h.knowledge.List()
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"categories": h.knowledge.List(),
|
"categories": categories,
|
||||||
"stats": h.knowledge.Stats(),
|
"stats": h.knowledge.Stats(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -795,13 +817,13 @@ func (h *Handler) handleTextMemory(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.lua == nil {
|
if h.adapter == nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"adapters": h.lua.ListAdapters()})
|
writeJSON(w, http.StatusOK, map[string]interface{}{"adapters": h.adapter.List()})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -811,12 +833,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
path := fmt.Sprintf("%s/%s.lua", h.lua.AdapterDir(), req.Name)
|
if err := h.adapter.Load(req.Name, req.Code); err != nil {
|
||||||
if err := os.WriteFile(path, []byte(req.Code), 0644); err != nil {
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.lua.LoadAdapter(path); err != nil {
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -827,7 +844,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.lua == nil {
|
if h.adapter == nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "lua vm not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -838,7 +855,7 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
for _, a := range h.lua.ListAdapters() {
|
for _, a := range h.adapter.List() {
|
||||||
if a.Name == name {
|
if a.Name == name {
|
||||||
writeJSON(w, http.StatusOK, a)
|
writeJSON(w, http.StatusOK, a)
|
||||||
return
|
return
|
||||||
@ -846,12 +863,10 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
path := fmt.Sprintf("%s/%s.lua", h.lua.AdapterDir(), name)
|
if err := h.adapter.Remove(name); err != nil {
|
||||||
if err := os.Remove(path); err != nil {
|
|
||||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "adapter not found"})
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": "adapter not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.lua.RemoveAdapter(name)
|
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name})
|
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name})
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
@ -865,7 +880,7 @@ func (h *Handler) handleNetwork(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"network_status": "monitoring",
|
"network_status": "monitoring",
|
||||||
"endpoints": h.config.Defaults.LLMEndpoints,
|
"endpoints": h.config.Get().Defaults.LLMEndpoints,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -876,10 +891,9 @@ func (h *Handler) addChatMsg(msg ChatMsg) {
|
|||||||
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
|
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
|
||||||
}
|
}
|
||||||
// persist to webui config table as compact JSON
|
// persist to webui config table as compact JSON
|
||||||
if h.cfgReg != nil {
|
if h.settings != nil {
|
||||||
ps := h.cfgReg.PluginConfig("webui")
|
|
||||||
b, _ := json.Marshal(h.chatHistory)
|
b, _ := json.Marshal(h.chatHistory)
|
||||||
ps.Set("chathistory", string(b))
|
_ = h.settings.Set("chathistory", string(b))
|
||||||
}
|
}
|
||||||
h.chatMu.Unlock()
|
h.chatMu.Unlock()
|
||||||
}
|
}
|
||||||
@ -929,7 +943,11 @@ func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.addChatMsg(ChatMsg{Role: "user", Content: body.Message, Time: time.Now().Format(time.RFC3339)})
|
h.addChatMsg(ChatMsg{Role: "user", Content: body.Message, Time: time.Now().Format(time.RFC3339)})
|
||||||
resp := h.iom.InjectTextSync("cli", body.Message)
|
if h.sdk == nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := h.sdk.InjectTextSync("cli", "cli", body.Message)
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
||||||
return
|
return
|
||||||
@ -965,7 +983,7 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
|
||||||
done := r.Context().Done()
|
done := r.Context().Done()
|
||||||
if h.eventBus == nil {
|
if h.sdk == nil {
|
||||||
fmt.Fprintf(w, "event: error\ndata: {\"msg\":\"event bus unavailable\"}\n\n")
|
fmt.Fprintf(w, "event: error\ndata: {\"msg\":\"event bus unavailable\"}\n\n")
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
return
|
return
|
||||||
@ -994,15 +1012,15 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
var unsubs []func()
|
var unsubs []func()
|
||||||
for _, t := range subTypes {
|
for _, t := range subTypes {
|
||||||
t2 := t
|
t2 := t
|
||||||
unsub := h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) {
|
unsub := h.sdk.Subscribe(sdk.EventType(t2), func(evt *sdk.Event) {
|
||||||
if evt.Type == events.EventToolCall {
|
if evt.Type == sdk.EventToolCall {
|
||||||
toolName, _ := evt.Payload["tool"].(string)
|
toolName, _ := evt.Payload["tool"].(string)
|
||||||
log.Printf("[SSE] received tool_call event: tool=%s", toolName)
|
log.Printf("[SSE] received tool_call event: tool=%s", toolName)
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(evt)
|
data, _ := json.Marshal(evt)
|
||||||
select {
|
select {
|
||||||
case writeCh <- fmt.Sprintf("event: %s\ndata: %s\n", evt.Type, string(data)):
|
case writeCh <- fmt.Sprintf("event: %s\ndata: %s\n", evt.Type, string(data)):
|
||||||
if evt.Type == events.EventToolCall {
|
if evt.Type == sdk.EventToolCall {
|
||||||
toolName, _ := evt.Payload["tool"].(string)
|
toolName, _ := evt.Payload["tool"].(string)
|
||||||
log.Printf("[SSE] wrote tool_call to writeCh: tool=%s", toolName)
|
log.Printf("[SSE] wrote tool_call to writeCh: tool=%s", toolName)
|
||||||
}
|
}
|
||||||
@ -1031,16 +1049,20 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config == nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "config not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
writeJSON(w, http.StatusOK, h.config)
|
writeJSON(w, http.StatusOK, h.config.Get())
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
var cfg types.Config
|
var cfg types.Config
|
||||||
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid config"})
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid config"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.config = &cfg
|
h.config.Put(&cfg)
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"status": "config_updated"})
|
writeJSON(w, http.StatusOK, map[string]string{"status": "config_updated"})
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
@ -1048,7 +1070,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.cfgReg == nil {
|
if h.settings == nil {
|
||||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "config registry not available"})
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": "config registry not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1056,30 +1078,35 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
prefix := r.URL.Query().Get("prefix")
|
prefix := r.URL.Query().Get("prefix")
|
||||||
values := make(map[string]interface{})
|
values := make(map[string]interface{})
|
||||||
meta := make(map[string]*internalConfig.ConfigDef)
|
meta := make(map[string]*sdk.ConfigDef)
|
||||||
|
|
||||||
if strings.HasPrefix(prefix, "plugin.") {
|
if strings.HasPrefix(prefix, "plugin.") {
|
||||||
// 插件配置:从插件自身 config_<name> 表读取
|
// 插件配置:从插件自身 config_<name> 表读取
|
||||||
pluginName := prefix[7:]
|
pluginName := prefix[7:]
|
||||||
ps := h.cfgReg.PluginConfig(pluginName)
|
keys, _ := h.settings.ListPlugin(pluginName, "")
|
||||||
keys, _ := ps.List("")
|
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
v, _ := ps.Get(k)
|
v, _ := h.settings.GetPlugin(pluginName, k)
|
||||||
fullKey := prefix + "." + k
|
fullKey := prefix + "." + k
|
||||||
values[fullKey] = v
|
values[fullKey] = v
|
||||||
if def := h.cfgReg.GetDef(fullKey); def != nil {
|
}
|
||||||
meta[fullKey] = def
|
for _, def := range h.settings.DefsPlugin(pluginName, "") {
|
||||||
}
|
fullKey := prefix + "." + def.Key
|
||||||
|
meta[fullKey] = def
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 核心配置:从 core config 表读取
|
// 核心配置:从 core config 表读取(键可为任意前缀,如 core.llm.*、webui.*)
|
||||||
keys := h.cfgReg.List(prefix)
|
all := h.settings.Dump()
|
||||||
for _, k := range keys {
|
var keys []string
|
||||||
v, _ := h.cfgReg.Get(k)
|
for k := range all {
|
||||||
values[k] = v
|
if strings.HasPrefix(k, prefix) {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
defs := h.cfgReg.ListDefs(prefix)
|
sort.Strings(keys)
|
||||||
for _, d := range defs {
|
for _, k := range keys {
|
||||||
|
values[k] = all[k]
|
||||||
|
}
|
||||||
|
for _, d := range h.settings.DefsCore(prefix) {
|
||||||
meta[d.Key] = d
|
meta[d.Key] = d
|
||||||
// 有 def 但 DB 中尚无值的 key,用 default 填充以便在 WebUI 中显示和编辑
|
// 有 def 但 DB 中尚无值的 key,用 default 填充以便在 WebUI 中显示和编辑
|
||||||
if _, exists := values[d.Key]; !exists {
|
if _, exists := values[d.Key]; !exists {
|
||||||
@ -1087,29 +1114,35 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 无前缀时同时加载所有插件配置
|
// 无前缀时同时加载所有插件配置
|
||||||
if prefix == "" && h.pluginReg != nil {
|
if prefix == "" {
|
||||||
for _, p := range h.pluginReg.List() {
|
for _, p := range h.settings.Plugins() {
|
||||||
ps := h.cfgReg.PluginConfig(p)
|
if p == "core" {
|
||||||
pkeys, _ := ps.List("")
|
continue
|
||||||
|
}
|
||||||
|
pkeys, _ := h.settings.ListPlugin(p, "")
|
||||||
for _, k := range pkeys {
|
for _, k := range pkeys {
|
||||||
v, _ := ps.Get(k)
|
v, _ := h.settings.GetPlugin(p, k)
|
||||||
fullKey := "plugin." + p + "." + k
|
fullKey := "plugin." + p + "." + k
|
||||||
values[fullKey] = v
|
values[fullKey] = v
|
||||||
if def := h.cfgReg.GetDef(fullKey); def != nil {
|
}
|
||||||
meta[fullKey] = def
|
for _, def := range h.settings.DefsPlugin(p, "") {
|
||||||
}
|
fullKey := "plugin." + p + "." + def.Key
|
||||||
|
meta[fullKey] = def
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins := []string{"core"}
|
plugins := []string{"core"}
|
||||||
pm := h.pluginReg.PluginMetas()
|
for _, p := range h.settings.Plugins() {
|
||||||
if h.pluginReg != nil {
|
if p != "core" {
|
||||||
for _, p := range h.pluginReg.List() {
|
|
||||||
plugins = append(plugins, "plugin."+p)
|
plugins = append(plugins, "plugin."+p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var pm map[string]sdk.PluginMeta
|
||||||
|
if h.pluginMgr != nil {
|
||||||
|
pm = h.pluginMgr.PluginMetas()
|
||||||
|
}
|
||||||
var disabledPlugins []sdk.DisabledPluginInfo
|
var disabledPlugins []sdk.DisabledPluginInfo
|
||||||
if h.pluginMgr != nil {
|
if h.pluginMgr != nil {
|
||||||
disabledPlugins = h.pluginMgr.ListDisabledPlugins()
|
disabledPlugins = h.pluginMgr.ListDisabledPlugins()
|
||||||
@ -1134,20 +1167,21 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
if strings.HasPrefix(body.Key, "plugin.") {
|
if strings.HasPrefix(body.Key, "plugin.") {
|
||||||
parts := strings.SplitN(body.Key, ".", 3)
|
parts := strings.SplitN(body.Key, ".", 3)
|
||||||
if len(parts) >= 3 {
|
if len(parts) >= 3 {
|
||||||
ps := h.cfgReg.PluginConfig(parts[1])
|
if err := h.settings.SetPlugin(parts[1], parts[2], body.Value); err != nil {
|
||||||
if err := ps.Set(parts[2], body.Value); err != nil {
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := h.cfgReg.Set(body.Key, body.Value); err != nil {
|
if err := h.settings.SetCore(body.Key, body.Value); err != nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(body.Key, "core.llm.") && h.providerMgr != nil && h.lua != nil {
|
if strings.HasPrefix(body.Key, "core.llm.") && h.llm != nil {
|
||||||
h.reloadLLMProviders()
|
if err := h.llm.ReloadFromConfig(); err != nil {
|
||||||
|
log.Printf("[webui] failed to reload LLM providers: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
default:
|
default:
|
||||||
@ -1155,36 +1189,13 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) reloadLLMProviders() {
|
|
||||||
cfg := h.cfgReg.ToConfig()
|
|
||||||
h.providerMgr.Reset()
|
|
||||||
for _, src := range cfg.LLM.Sources {
|
|
||||||
key := src.APIKey
|
|
||||||
if key == "" {
|
|
||||||
key = h.baseAPIKey
|
|
||||||
}
|
|
||||||
provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
|
|
||||||
Model: src.Model,
|
|
||||||
BaseURL: src.BaseURL,
|
|
||||||
APIKey: key,
|
|
||||||
Temperature: cfg.LLM.Temperature,
|
|
||||||
MaxTokens: cfg.LLM.MaxTokens,
|
|
||||||
ContextWindow: src.ContextWindow,
|
|
||||||
}, h.lua, src.Adapter)
|
|
||||||
h.providerMgr.Register(src.Name, provider)
|
|
||||||
}
|
|
||||||
if cfg.LLM.Provider != "" {
|
|
||||||
_ = h.providerMgr.SetDefault(cfg.LLM.Provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.iom == nil {
|
if h.sdk == nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "IO manager not available"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "IO manager not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1211,9 +1222,9 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := h.iom.InjectTextSync("http", lastMsg.Content)
|
response := h.sdk.InjectTextSync("http", "http", lastMsg.Content)
|
||||||
if response == nil {
|
if response == nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "no response from agent"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "no response from agent"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1375,11 +1386,10 @@ func (h *Handler) handleTracker(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (h *Handler) pluginmgrAddr() string {
|
func (h *Handler) pluginmgrAddr() string {
|
||||||
addr := "127.0.0.1:9876"
|
addr := "127.0.0.1:9876"
|
||||||
if h.cfgReg == nil {
|
if h.settings == nil {
|
||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
ps := h.cfgReg.PluginConfig("pluginmgr")
|
if v, err := h.settings.GetPlugin("pluginmgr", "http_addr"); err == nil {
|
||||||
if v, err := ps.Get("http_addr"); err == nil {
|
|
||||||
if s, ok := v.(string); ok && s != "" {
|
if s, ok := v.(string); ok && s != "" {
|
||||||
addr = s
|
addr = s
|
||||||
}
|
}
|
||||||
@ -1436,17 +1446,11 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if path == "reload" && r.Method == http.MethodPost {
|
if path == "reload" && r.Method == http.MethodPost {
|
||||||
if h.pluginReg == nil {
|
if h.pluginMgr == nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin registry not available"})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin registry not available"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dir := ""
|
if _, err := h.pluginMgr.ReloadPlugins(); err != nil {
|
||||||
if h.cfgReg != nil {
|
|
||||||
if v, _ := h.cfgReg.Get("core.plugin.dir"); v != nil {
|
|
||||||
dir, _ = v.(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := h.pluginReg.Reload(dir); err != nil {
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,19 @@ import (
|
|||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testSDK(cfg sdk.SDKConfig) *sdk.PluginSDK {
|
||||||
|
if cfg.EventBus == nil {
|
||||||
|
cfg.EventBus = events.NewBus()
|
||||||
|
}
|
||||||
|
return sdk.New("webui", cfg)
|
||||||
|
}
|
||||||
|
|
||||||
func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) {
|
func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cfg := &types.Config{
|
cfg := &types.Config{
|
||||||
@ -34,7 +41,11 @@ func newTestHandler(t *testing.T) (*Handler, *supervisor.Daemon) {
|
|||||||
sup := supervisor.New(cfg)
|
sup := supervisor.New(cfg)
|
||||||
sup.Start()
|
sup.Start()
|
||||||
|
|
||||||
return NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, ""), sup
|
s := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Config: sdk.NewConfig(cfg),
|
||||||
|
})
|
||||||
|
return NewHandler(s), sup
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthMiddleware(t *testing.T) {
|
func TestAuthMiddleware(t *testing.T) {
|
||||||
@ -44,9 +55,21 @@ func TestAuthMiddleware(t *testing.T) {
|
|||||||
cfgReg.PluginConfig("webui").Set("password", "secret-pass")
|
cfgReg.PluginConfig("webui").Set("password", "secret-pass")
|
||||||
cfgReg.PluginConfig("webui").Set("session_ttl_hours", "24")
|
cfgReg.PluginConfig("webui").Set("session_ttl_hours", "24")
|
||||||
|
|
||||||
h, sup := newTestHandler(t)
|
sup := supervisor.New(&types.Config{
|
||||||
|
Daemon: types.DaemonConfig{
|
||||||
|
CheckInterval: time.Minute,
|
||||||
|
HeartbeatInterval: 30 * time.Second,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
h.cfgReg = cfgReg
|
|
||||||
|
s := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Settings: sdk.NewSettings("webui", cfgReg),
|
||||||
|
Config: sdk.NewConfig(&types.Config{}),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
h.RegisterRoutes(mux)
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
@ -194,7 +217,12 @@ func TestHandleKnowledgeSearch(t *testing.T) {
|
|||||||
sup.Start()
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
|
|
||||||
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "")
|
s := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Knowledge: sdk.NewKnowledge(ks),
|
||||||
|
Config: sdk.NewConfig(cfg),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/knowledge?q=test", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/knowledge?q=test", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -226,7 +254,12 @@ func TestHandleKnowledgeCreate(t *testing.T) {
|
|||||||
sup.Start()
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
|
|
||||||
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, ks, nil, nil, nil, events.NewBus(), nil, nil, "")
|
s := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Knowledge: sdk.NewKnowledge(ks),
|
||||||
|
Config: sdk.NewConfig(cfg),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
|
|
||||||
body := `{"name":"new_doc","content":"fresh content"}`
|
body := `{"name":"new_doc","content":"fresh content"}`
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/knowledge", strings.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/knowledge", strings.NewReader(body))
|
||||||
@ -291,7 +324,12 @@ func TestHandleTrackerStats(t *testing.T) {
|
|||||||
sup.Start()
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
|
|
||||||
h := NewHandler(sup, nil, nil, nil, cfg, nil, nil, nil, tr, nil, nil, events.NewBus(), nil, nil, "")
|
s := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Tracker: tr,
|
||||||
|
Config: sdk.NewConfig(cfg),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/tracker", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/tracker", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -305,8 +343,6 @@ func TestHandleTrackerStats(t *testing.T) {
|
|||||||
func TestHandleOpenAICompletionsNoMessages(t *testing.T) {
|
func TestHandleOpenAICompletionsNoMessages(t *testing.T) {
|
||||||
h, sup := newTestHandler(t)
|
h, sup := newTestHandler(t)
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
// 给 handler 一个 IOManager,才能通过 nil 检查到达消息校验
|
|
||||||
h.iom = agentIO.NewIOManager()
|
|
||||||
|
|
||||||
body := `{"model":"test"}`
|
body := `{"model":"test"}`
|
||||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
|
||||||
@ -322,8 +358,6 @@ func TestHandleOpenAICompletionsNoMessages(t *testing.T) {
|
|||||||
func TestHandleOpenAICompletionsLastMsgNotUser(t *testing.T) {
|
func TestHandleOpenAICompletionsLastMsgNotUser(t *testing.T) {
|
||||||
h, sup := newTestHandler(t)
|
h, sup := newTestHandler(t)
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
// 给 handler 一个 IOManager,才能通过 nil 检查到达消息校验
|
|
||||||
h.iom = agentIO.NewIOManager()
|
|
||||||
|
|
||||||
body := `{"messages":[{"role":"assistant","content":"hi"}]}`
|
body := `{"messages":[{"role":"assistant","content":"hi"}]}`
|
||||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
|
||||||
@ -392,9 +426,27 @@ func TestHandleConfigGet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterRoutes(t *testing.T) {
|
func TestRegisterRoutes(t *testing.T) {
|
||||||
h, sup := newTestHandler(t)
|
cfgReg := internalConfig.NewConfigRegistry("")
|
||||||
|
cfgReg.PluginConfig("webui").Set("api_key", "test-api-key")
|
||||||
|
cfgReg.PluginConfig("webui").Set("username", "admin")
|
||||||
|
cfgReg.PluginConfig("webui").Set("password", "secret-pass")
|
||||||
|
|
||||||
|
sup := supervisor.New(&types.Config{
|
||||||
|
Daemon: types.DaemonConfig{
|
||||||
|
CheckInterval: time.Minute,
|
||||||
|
HeartbeatInterval: 30 * time.Second,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
|
|
||||||
|
s := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Settings: sdk.NewSettings("webui", cfgReg),
|
||||||
|
Config: sdk.NewConfig(&types.Config{}),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
h.RegisterRoutes(mux)
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
@ -407,7 +459,7 @@ func TestRegisterRoutes(t *testing.T) {
|
|||||||
{"/api/v1/agents", http.MethodGet, http.StatusOK},
|
{"/api/v1/agents", http.MethodGet, http.StatusOK},
|
||||||
{"/api/v1/config", http.MethodGet, http.StatusOK},
|
{"/api/v1/config", http.MethodGet, http.StatusOK},
|
||||||
{"/api/v1/network", http.MethodGet, http.StatusOK},
|
{"/api/v1/network", http.MethodGet, http.StatusOK},
|
||||||
{"/", http.MethodGet, http.StatusOK},
|
{"/", http.MethodGet, http.StatusFound},
|
||||||
{"/api/v1/memory", http.MethodGet, http.StatusServiceUnavailable},
|
{"/api/v1/memory", http.MethodGet, http.StatusServiceUnavailable},
|
||||||
{"/api/v1/knowledge", http.MethodGet, http.StatusServiceUnavailable},
|
{"/api/v1/knowledge", http.MethodGet, http.StatusServiceUnavailable},
|
||||||
{"/api/v1/tracker", http.MethodGet, http.StatusServiceUnavailable},
|
{"/api/v1/tracker", http.MethodGet, http.StatusServiceUnavailable},
|
||||||
@ -416,6 +468,9 @@ func TestRegisterRoutes(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
req := httptest.NewRequest(tt.method, tt.path, nil)
|
req := httptest.NewRequest(tt.method, tt.path, nil)
|
||||||
|
if strings.HasPrefix(tt.path, "/api/v1/") {
|
||||||
|
req.Header.Set("X-API-Key", "test-api-key")
|
||||||
|
}
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
mux.ServeHTTP(w, req)
|
mux.ServeHTTP(w, req)
|
||||||
|
|
||||||
@ -470,8 +525,12 @@ func TestSettingsAPIFlow(t *testing.T) {
|
|||||||
sup.Start()
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
|
|
||||||
pluginReg := plugin.NewRegistry()
|
s := testSDK(sdk.SDKConfig{
|
||||||
h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "")
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Settings: sdk.NewSettings("webui", cfgReg),
|
||||||
|
Config: sdk.NewConfig(&types.Config{}),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
|
|
||||||
t.Run("GET_settings_lists_keys_and_plugins", func(t *testing.T) {
|
t.Run("GET_settings_lists_keys_and_plugins", func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
|
||||||
@ -578,7 +637,11 @@ func TestSettingsAPIFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("settings_not_available_without_registry", func(t *testing.T) {
|
t.Run("settings_not_available_without_registry", func(t *testing.T) {
|
||||||
h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
|
s2 := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Config: sdk.NewConfig(&types.Config{}),
|
||||||
|
})
|
||||||
|
h2 := NewHandler(s2)
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h2.handleSettings(w, req)
|
h2.handleSettings(w, req)
|
||||||
@ -603,8 +666,12 @@ func TestSettingsWithPluginRegistry(t *testing.T) {
|
|||||||
sup.Start()
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
|
|
||||||
pluginReg := plugin.NewRegistry()
|
s := testSDK(sdk.SDKConfig{
|
||||||
h := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, cfgReg, pluginReg, events.NewBus(), nil, nil, "")
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
Settings: sdk.NewSettings("webui", cfgReg),
|
||||||
|
Config: sdk.NewConfig(&types.Config{}),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/settings", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -633,7 +700,7 @@ type echoProvider struct{ name string }
|
|||||||
func (p *echoProvider) Name() string { return p.name }
|
func (p *echoProvider) Name() string { return p.name }
|
||||||
func (p *echoProvider) MaxContextTokens() int { return 8192 }
|
func (p *echoProvider) MaxContextTokens() int { return 8192 }
|
||||||
func (p *echoProvider) Chat(ctx context.Context, req *agentAPI.CompletionRequest) (*agentAPI.CompletionResponse, error) {
|
func (p *echoProvider) Chat(ctx context.Context, req *agentAPI.CompletionRequest) (*agentAPI.CompletionResponse, error) {
|
||||||
content := "echo: " + req.Messages[len(req.Messages)-1].Content
|
content := "echo: " + lastUserContent(req.Messages)
|
||||||
return &agentAPI.CompletionResponse{Content: content, FinishReason: "stop"}, nil
|
return &agentAPI.CompletionResponse{Content: content, FinishReason: "stop"}, nil
|
||||||
}
|
}
|
||||||
func (p *echoProvider) ChatStream(ctx context.Context, req *agentAPI.CompletionRequest) (<-chan agentAPI.StreamChunk, error) {
|
func (p *echoProvider) ChatStream(ctx context.Context, req *agentAPI.CompletionRequest) (<-chan agentAPI.StreamChunk, error) {
|
||||||
@ -642,6 +709,15 @@ func (p *echoProvider) ChatStream(ctx context.Context, req *agentAPI.CompletionR
|
|||||||
return ch, nil
|
return ch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lastUserContent(msgs []agentAPI.Message) string {
|
||||||
|
for i := len(msgs) - 1; i >= 0; i-- {
|
||||||
|
if msgs[i].Role == "user" {
|
||||||
|
return msgs[i].Content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// 避免测试时自动输出
|
// 避免测试时自动输出
|
||||||
}
|
}
|
||||||
@ -656,19 +732,22 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer memDB.Close()
|
defer memDB.Close()
|
||||||
|
|
||||||
|
pm := agentAPI.NewProviderManager()
|
||||||
|
pm.Register("echo", &echoProvider{name: "echo"})
|
||||||
|
|
||||||
agent := agentCore.New(agentCore.AgentConfig{
|
agent := agentCore.New(agentCore.AgentConfig{
|
||||||
ID: "test",
|
ID: "test",
|
||||||
SystemPrompt: "你是测试助手",
|
SystemPrompt: "你是测试助手",
|
||||||
Provider: &echoProvider{name: "echo"},
|
Provider: &echoProvider{name: "echo"},
|
||||||
IO: iom,
|
ProviderManager: pm,
|
||||||
Memory: memDB,
|
IO: iom,
|
||||||
Indexer: nil,
|
Memory: memDB,
|
||||||
|
Indexer: nil,
|
||||||
ContextSavePath: "",
|
ContextSavePath: "",
|
||||||
})
|
})
|
||||||
agent.Start()
|
agent.Start()
|
||||||
defer agent.Stop()
|
defer agent.Stop()
|
||||||
|
|
||||||
// Handler 需要 iom
|
|
||||||
sup := supervisor.New(&types.Config{
|
sup := supervisor.New(&types.Config{
|
||||||
Daemon: types.DaemonConfig{
|
Daemon: types.DaemonConfig{
|
||||||
CheckInterval: time.Minute,
|
CheckInterval: time.Minute,
|
||||||
@ -678,7 +757,12 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
|
|||||||
sup.Start()
|
sup.Start()
|
||||||
defer sup.Shutdown()
|
defer sup.Shutdown()
|
||||||
|
|
||||||
h := NewHandler(sup, nil, nil, nil, &types.Config{}, iom, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
|
s := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
IOManager: iom,
|
||||||
|
Config: sdk.NewConfig(&types.Config{}),
|
||||||
|
})
|
||||||
|
h := NewHandler(s)
|
||||||
|
|
||||||
t.Run("POST_chat_completions_returns_echo", func(t *testing.T) {
|
t.Run("POST_chat_completions_returns_echo", func(t *testing.T) {
|
||||||
body := `{"model":"test","messages":[{"role":"user","content":"你好"}]}`
|
body := `{"model":"test","messages":[{"role":"user","content":"你好"}]}`
|
||||||
@ -711,7 +795,10 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("POST_chat_completions_no_iom_returns_503", func(t *testing.T) {
|
t.Run("POST_chat_completions_no_iom_returns_503", func(t *testing.T) {
|
||||||
h2 := NewHandler(sup, nil, nil, nil, &types.Config{}, nil, nil, nil, nil, nil, nil, events.NewBus(), nil, nil, "")
|
s2 := testSDK(sdk.SDKConfig{
|
||||||
|
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||||
|
})
|
||||||
|
h2 := NewHandler(s2)
|
||||||
body := `{"messages":[{"role":"user","content":"hi"}]}`
|
body := `{"messages":[{"role":"user","content":"hi"}]}`
|
||||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|||||||
@ -7,117 +7,28 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
|
||||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
|
||||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
|
||||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
|
||||||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 包级依赖注入 — 由 main.go 在 Load() 前调用 Configure() 设置。
|
|
||||||
var (
|
|
||||||
webuiAddr string
|
|
||||||
webuiSup *supervisor.Daemon
|
|
||||||
webuiMem *memory.GraphDB
|
|
||||||
webuiSK *skill.Manager
|
|
||||||
webuiLua *luaVM.VM
|
|
||||||
webuiCfg *types.Config
|
|
||||||
webuiIOM *agentIO.IOManager
|
|
||||||
webuiTM *text.Memory
|
|
||||||
webuiKS *knowledge.Store
|
|
||||||
webuiTR *tracker.Tracker
|
|
||||||
webuiCR *internalConfig.ConfigRegistry
|
|
||||||
webuiPR *plugin.Registry
|
|
||||||
webuiEvBus *events.Bus
|
|
||||||
webuiStatusProvider agentCore.StatusProvider
|
|
||||||
webuiProviderMgr *agentAPI.ProviderManager
|
|
||||||
webuiBaseAPIKey string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Configure 注入 WebUI 插件需要的内核依赖。必须在 Load() 之前调用。
|
|
||||||
func Configure(addr string,
|
|
||||||
sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
|
|
||||||
lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager,
|
|
||||||
tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker,
|
|
||||||
cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus,
|
|
||||||
sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string,
|
|
||||||
) {
|
|
||||||
webuiAddr = addr
|
|
||||||
webuiSup, webuiMem, webuiSK, webuiLua = sup, mem, sk, lua
|
|
||||||
webuiCfg, webuiIOM, webuiTM, webuiKS = cfg, iom, tm, ks
|
|
||||||
webuiTR, webuiCR, webuiPR, webuiEvBus = tr, cr, pr, evBus
|
|
||||||
webuiStatusProvider = sp
|
|
||||||
webuiProviderMgr = pm
|
|
||||||
webuiBaseAPIKey = baseKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
plugin.RegisterPluginMeta("webui", "Web 控制台", "WebUI")
|
plugin.RegisterPluginMeta("webui", "Web 控制台", "WebUI")
|
||||||
plugin.RegisterFactory("webui", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
plugin.RegisterFactory("webui", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
if webuiSup == nil {
|
return New(name), nil
|
||||||
return nil, nil // 未 Configure 则跳过(不给日志警告)
|
|
||||||
}
|
|
||||||
addr := webuiAddr
|
|
||||||
if a, ok := config["addr"].(string); ok {
|
|
||||||
addr = a
|
|
||||||
}
|
|
||||||
return New(name, addr,
|
|
||||||
webuiSup, webuiMem, webuiSK, webuiLua,
|
|
||||||
webuiCfg, webuiIOM, webuiTM, webuiKS,
|
|
||||||
webuiTR, webuiCR, webuiPR, webuiEvBus, webuiStatusProvider,
|
|
||||||
webuiProviderMgr, webuiBaseAPIKey,
|
|
||||||
), nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
name string
|
name string
|
||||||
addr string
|
|
||||||
handler *Handler
|
handler *Handler
|
||||||
server *http.Server
|
server *http.Server
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
|
|
||||||
sup *supervisor.Daemon
|
|
||||||
mem *memory.GraphDB
|
|
||||||
sk *skill.Manager
|
|
||||||
lua *luaVM.VM
|
|
||||||
cfg *types.Config
|
|
||||||
iom *agentIO.IOManager
|
|
||||||
tm *text.Memory
|
|
||||||
ks *knowledge.Store
|
|
||||||
tr *tracker.Tracker
|
|
||||||
cr *internalConfig.ConfigRegistry
|
|
||||||
pr *plugin.Registry
|
|
||||||
evBus *events.Bus
|
|
||||||
statusProvider agentCore.StatusProvider
|
|
||||||
providerMgr *agentAPI.ProviderManager
|
|
||||||
baseAPIKey string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(name, addr string,
|
func New(name string) *Plugin {
|
||||||
sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
|
|
||||||
lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager,
|
|
||||||
tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker,
|
|
||||||
cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus,
|
|
||||||
sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string,
|
|
||||||
) *Plugin {
|
|
||||||
return &Plugin{
|
return &Plugin{
|
||||||
name: name,
|
name: name,
|
||||||
addr: addr,
|
|
||||||
mux: http.NewServeMux(),
|
mux: http.NewServeMux(),
|
||||||
sup: sup, mem: mem, sk: sk, lua: lua, cfg: cfg,
|
|
||||||
iom: iom, tm: tm, ks: ks, tr: tr, cr: cr, pr: pr, evBus: evBus,
|
|
||||||
statusProvider: sp, providerMgr: pm, baseAPIKey: baseKey,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,11 +68,18 @@ func (p *Plugin) Name() string { return p.name }
|
|||||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||||
s.SetAutoRestart(true)
|
s.SetAutoRestart(true)
|
||||||
|
|
||||||
|
addr := ":8080"
|
||||||
|
if v, _ := s.Settings().Get("addr"); v != nil {
|
||||||
|
if s2, ok := v.(string); ok && s2 != "" {
|
||||||
|
addr = s2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.RegisterOutputChannel("webui", 1, "Web 控制台", sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
|
s.RegisterOutputChannel("webui", 1, "Web 控制台", sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
|
||||||
payload, _ := args["payload"].(string)
|
payload, _ := args["payload"].(string)
|
||||||
if payload != "" {
|
if payload != "" {
|
||||||
p.evBus.Publish(&events.Event{
|
s.Publish(&sdk.Event{
|
||||||
Type: events.EventAgentOutput,
|
Type: sdk.EventAgentOutput,
|
||||||
Payload: map[string]interface{}{
|
Payload: map[string]interface{}{
|
||||||
"content": payload,
|
"content": payload,
|
||||||
"channel": "webui",
|
"channel": "webui",
|
||||||
@ -171,6 +89,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
return map[string]interface{}{"status": "ok"}, nil
|
return map[string]interface{}{"status": "ok"}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.Settings().RegisterDef(sdk.ConfigDef{Key: "addr", Default: ":8080", Type: "string", DisplayName: "监听地址", Description: "Web 控制台监听地址", Category: "webui"})
|
||||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "api_key", Default: "", Type: "password", DisplayName: "API 密钥", Description: "访问 API 时需要的密钥", Category: "webui"})
|
s.Settings().RegisterDef(sdk.ConfigDef{Key: "api_key", Default: "", Type: "password", DisplayName: "API 密钥", Description: "访问 API 时需要的密钥", Category: "webui"})
|
||||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "username", Default: "admin", Type: "string", DisplayName: "登录用户名", Description: "Web 控制台登录用户名", Category: "webui"})
|
s.Settings().RegisterDef(sdk.ConfigDef{Key: "username", Default: "admin", Type: "string", DisplayName: "登录用户名", Description: "Web 控制台登录用户名", Category: "webui"})
|
||||||
s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"})
|
s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"})
|
||||||
@ -178,7 +97,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
p.ensureAuthBootstrap(s)
|
p.ensureAuthBootstrap(s)
|
||||||
|
|
||||||
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
|
s.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error {
|
||||||
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "pre_action", "message": "thinking"}})
|
s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "pre_action", "message": "thinking"}})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
s.RegisterStage(sdk.StageBeforeToolcall, func(ctx *sdk.StageContext) error {
|
s.RegisterStage(sdk.StageBeforeToolcall, func(ctx *sdk.StageContext) error {
|
||||||
@ -186,22 +105,20 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
if len(ctx.ToolCalls) > 0 {
|
if len(ctx.ToolCalls) > 0 {
|
||||||
tool = ctx.ToolCalls[0].Name
|
tool = ctx.ToolCalls[0].Name
|
||||||
}
|
}
|
||||||
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_toolcall", "tool": tool, "message": "tool:" + tool}})
|
s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "before_toolcall", "tool": tool, "message": "tool:" + tool}})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
s.RegisterStage(sdk.StageBeforeOutput, func(ctx *sdk.StageContext) error {
|
s.RegisterStage(sdk.StageBeforeOutput, func(ctx *sdk.StageContext) error {
|
||||||
p.evBus.Publish(&events.Event{Type: events.EventStage, Payload: map[string]interface{}{"phase": "before_output", "message": "output"}})
|
s.Publish(&sdk.Event{Type: sdk.EventStage, Payload: map[string]interface{}{"phase": "before_output", "message": "output"}})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
h := NewHandler(p.sup, p.mem, p.sk, p.lua, p.cfg, p.iom, p.tm, p.ks, p.tr, p.cr, p.pr, p.evBus, p.statusProvider, p.providerMgr, p.baseAPIKey)
|
p.handler = NewHandler(s)
|
||||||
h.SetPluginMgr(s.PluginMgr())
|
p.handler.RegisterRoutes(p.mux)
|
||||||
p.handler = h
|
|
||||||
h.RegisterRoutes(p.mux)
|
|
||||||
|
|
||||||
p.server = &http.Server{Addr: p.addr, Handler: p.mux}
|
p.server = &http.Server{Addr: addr, Handler: p.mux}
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("[webui] HTTP server listening on %s", p.addr)
|
log.Printf("[webui] HTTP server listening on %s", addr)
|
||||||
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Printf("[webui] server error: %v", err)
|
log.Printf("[webui] server error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
16
internal/sdk/adapter.go
Normal file
16
internal/sdk/adapter.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
// AdapterAPI provides access to the Lua LLM protocol adapters.
|
||||||
|
type AdapterAPI interface {
|
||||||
|
List() []APIAdapter
|
||||||
|
// Load writes the adapter source file and hot-reloads it.
|
||||||
|
Load(name, code string) error
|
||||||
|
Remove(name string) error
|
||||||
|
AdapterDir() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIAdapter describes a loaded Lua protocol adapter.
|
||||||
|
type APIAdapter struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
62
internal/sdk/adapter_impl.go
Normal file
62
internal/sdk/adapter_impl.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
// adapterImpl 桥接 Lua VM 的协议适配器管理。
|
||||||
|
type adapterImpl struct {
|
||||||
|
vm *luaVM.VM
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(vm *luaVM.VM) AdapterAPI {
|
||||||
|
return &adapterImpl{vm: vm}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapterImpl) List() []APIAdapter {
|
||||||
|
if a.vm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
got := a.vm.ListAdapters()
|
||||||
|
out := make([]APIAdapter, len(got))
|
||||||
|
for i, ad := range got {
|
||||||
|
out[i] = APIAdapter{Name: ad.Name, Version: ad.Version}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapterImpl) Load(name, code string) error {
|
||||||
|
if a.vm == nil {
|
||||||
|
return errors.New("lua vm not available")
|
||||||
|
}
|
||||||
|
path := filepath.Join(a.vm.AdapterDir(), name+".lua")
|
||||||
|
if err := os.WriteFile(path, []byte(code), 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return a.vm.LoadAdapter(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapterImpl) Remove(name string) error {
|
||||||
|
if a.vm == nil {
|
||||||
|
return errors.New("lua vm not available")
|
||||||
|
}
|
||||||
|
path := filepath.Join(a.vm.AdapterDir(), name+".lua")
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.vm.RemoveAdapter(name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adapterImpl) AdapterDir() string {
|
||||||
|
if a.vm == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.vm.AdapterDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AdapterAPI = (*adapterImpl)(nil)
|
||||||
10
internal/sdk/config.go
Normal file
10
internal/sdk/config.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import "gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
|
|
||||||
|
// ConfigAPI provides read/write access to the runtime config object
|
||||||
|
// (unrelated to the config database).
|
||||||
|
type ConfigAPI interface {
|
||||||
|
Get() *types.Config
|
||||||
|
Put(cfg *types.Config)
|
||||||
|
}
|
||||||
22
internal/sdk/config_impl.go
Normal file
22
internal/sdk/config_impl.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import "gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
|
|
||||||
|
// configImpl 持有运行期配置对象的引用(与配置数据库无关)。
|
||||||
|
type configImpl struct {
|
||||||
|
cfg *types.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(cfg *types.Config) ConfigAPI {
|
||||||
|
return &configImpl{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configImpl) Get() *types.Config {
|
||||||
|
return c.cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configImpl) Put(cfg *types.Config) {
|
||||||
|
c.cfg = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ConfigAPI = (*configImpl)(nil)
|
||||||
20
internal/sdk/events.go
Normal file
20
internal/sdk/events.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import "gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||||
|
|
||||||
|
// 事件类型/事件类型别名:插件经 SDK 订阅/发布内核事件,
|
||||||
|
// 无需直接 import internal/events(内核事件总线仅通过 SDK 暴露)。
|
||||||
|
|
||||||
|
type EventType = events.EventType
|
||||||
|
type Event = events.Event
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventRawInput = events.EventRawInput
|
||||||
|
EventAgentOutput = events.EventAgentOutput
|
||||||
|
EventAgentLLMChain = events.EventAgentLLMChain
|
||||||
|
EventToolCall = events.EventToolCall
|
||||||
|
EventReasoning = events.EventReasoning
|
||||||
|
EventStage = events.EventStage
|
||||||
|
EventSystem = events.EventSystem
|
||||||
|
EventAll = events.EventAll
|
||||||
|
)
|
||||||
17
internal/sdk/indexer.go
Normal file
17
internal/sdk/indexer.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
// IndexerAPI exposes the memory context indexer.
|
||||||
|
type IndexerAPI interface {
|
||||||
|
BuildContext(q string) (*IndexContext, error)
|
||||||
|
FormatContext(ctx *IndexContext) string
|
||||||
|
GetToolDefinitions() []map[string]interface{}
|
||||||
|
BuildToolPrompt() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexContext is a neutral snapshot of the injected memory context.
|
||||||
|
type IndexContext struct {
|
||||||
|
Entities []Entity `json:"entities"`
|
||||||
|
Relations []Relation `json:"relations"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
TokenEstimate int `json:"token_estimate"`
|
||||||
|
}
|
||||||
71
internal/sdk/indexer_impl.go
Normal file
71
internal/sdk/indexer_impl.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// indexerImpl 桥接 memory.Indexer 到中立的 IndexerAPI。
|
||||||
|
type indexerImpl struct {
|
||||||
|
idx *memory.Indexer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndexer(idx *memory.Indexer) IndexerAPI {
|
||||||
|
return &indexerImpl{idx: idx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexerImpl) BuildContext(q string) (*IndexContext, error) {
|
||||||
|
if i.idx == nil {
|
||||||
|
return nil, errors.New("indexer not available")
|
||||||
|
}
|
||||||
|
ctx := i.idx.BuildContext(q)
|
||||||
|
if ctx == nil {
|
||||||
|
return &IndexContext{}, nil
|
||||||
|
}
|
||||||
|
entities := make([]Entity, len(ctx.Entities))
|
||||||
|
for j, e := range ctx.Entities {
|
||||||
|
entities[j] = Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount}
|
||||||
|
}
|
||||||
|
relations := make([]Relation, len(ctx.Relations))
|
||||||
|
for j, r := range ctx.Relations {
|
||||||
|
relations[j] = Relation{SourceName: r.SourceName, TargetName: r.TargetName, RelationType: r.RelationType}
|
||||||
|
}
|
||||||
|
return &IndexContext{
|
||||||
|
Entities: entities,
|
||||||
|
Relations: relations,
|
||||||
|
Summary: ctx.Summary,
|
||||||
|
TokenEstimate: ctx.TokenEstimate,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexerImpl) FormatContext(ctx *IndexContext) string {
|
||||||
|
if i.idx == nil || ctx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
entities := make([]memory.Entity, len(ctx.Entities))
|
||||||
|
for j, e := range ctx.Entities {
|
||||||
|
entities[j] = memory.Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount}
|
||||||
|
}
|
||||||
|
return i.idx.FormatContext(&memory.InjectedContext{
|
||||||
|
Entities: entities,
|
||||||
|
Summary: ctx.Summary,
|
||||||
|
TokenEstimate: ctx.TokenEstimate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexerImpl) GetToolDefinitions() []map[string]interface{} {
|
||||||
|
if i.idx == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return i.idx.GetToolDefinitions()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *indexerImpl) BuildToolPrompt() string {
|
||||||
|
if i.idx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return i.idx.BuildToolPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ IndexerAPI = (*indexerImpl)(nil)
|
||||||
@ -2,5 +2,13 @@ package sdk
|
|||||||
|
|
||||||
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||||
|
|
||||||
type KnowledgeAPI = pubsdk.KnowledgeAPI
|
// KnowledgeAPI 是内置插件使用的全量知识库接口。
|
||||||
|
type KnowledgeAPI interface {
|
||||||
|
pubsdk.KnowledgeAPI
|
||||||
|
// Stats 返回知识库的运行统计。
|
||||||
|
Stats() map[string]interface{}
|
||||||
|
// Remove 按名称删除一条知识。
|
||||||
|
Remove(name string) error
|
||||||
|
}
|
||||||
|
|
||||||
type Knowledge = pubsdk.Knowledge
|
type Knowledge = pubsdk.Knowledge
|
||||||
|
|||||||
@ -26,4 +26,14 @@ func (k *knowledgeImpl) List() ([]string, error) {
|
|||||||
return k.ks.List(), nil
|
return k.ks.List(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k *knowledgeImpl) Stats() map[string]interface{} {
|
||||||
|
if k.ks == nil { return map[string]interface{}{} }
|
||||||
|
return k.ks.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *knowledgeImpl) Remove(name string) error {
|
||||||
|
if k.ks == nil { return nil }
|
||||||
|
return k.ks.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
var _ KnowledgeAPI = (*knowledgeImpl)(nil)
|
var _ KnowledgeAPI = (*knowledgeImpl)(nil)
|
||||||
|
|||||||
@ -1,5 +1,62 @@
|
|||||||
package sdk
|
package sdk
|
||||||
|
|
||||||
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
type LLMAPI = pubsdk.LLMAPI
|
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LLMAPI 是内置插件使用的全量 LLM Provider 接口。
|
||||||
|
// 内置 SDK 为全量接口,外部 SDK 仅暴露其中的安全子集(pubsdk.LLMAPI)。
|
||||||
|
type LLMAPI interface {
|
||||||
|
pubsdk.LLMAPI
|
||||||
|
// ReloadFromConfig 从内核配置重建所有 LLM Provider。
|
||||||
|
ReloadFromConfig() error
|
||||||
|
// Chat 向默认 Provider 发起一次补全调用(非记忆管线)。
|
||||||
|
Chat(ctx context.Context, req *LLMCompletionRequest) (*LLMCompletionResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMMessage 是面向 LLM Provider 的中立对话消息。
|
||||||
|
type LLMMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
|
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMToolCall 是中立的工具调用请求。
|
||||||
|
type LLMToolCall struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Arguments map[string]interface{} `json:"arguments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMTokenUsage 报告一次补全的 token 消耗。
|
||||||
|
type LLMTokenUsage struct {
|
||||||
|
Prompt int `json:"prompt"`
|
||||||
|
Completion int `json:"completion"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMCompletionRequest 是中立的补全请求。
|
||||||
|
// Tools/ToolChoice 使用 OpenAI 风格载荷,便于 Provider 直接转发。
|
||||||
|
type LLMCompletionRequest struct {
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
Messages []LLMMessage `json:"messages"`
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
MaxTokens int `json:"max_tokens,omitempty"`
|
||||||
|
Stream bool `json:"stream,omitempty"`
|
||||||
|
Tools []interface{} `json:"tools,omitempty"`
|
||||||
|
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
||||||
|
DisableThinking bool `json:"disable_thinking,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMCompletionResponse 是中立的补全响应。
|
||||||
|
type LLMCompletionResponse struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
|
FinishReason string `json:"finish_reason,omitempty"`
|
||||||
|
TokenUsage LLMTokenUsage `json:"token_usage,omitempty"`
|
||||||
|
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@ -1,26 +1,133 @@
|
|||||||
package sdk
|
package sdk
|
||||||
|
|
||||||
import agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
type llmImpl struct{ mgr *agentAPI.ProviderManager }
|
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||||
|
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||||
|
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||||||
|
)
|
||||||
|
|
||||||
func NewLLM(mgr *agentAPI.ProviderManager) LLMAPI { return &llmImpl{mgr: mgr} }
|
type llmImpl struct {
|
||||||
|
mgr *agentAPI.ProviderManager
|
||||||
|
cfgReg *internalConfig.ConfigRegistry
|
||||||
|
lua *luaVM.VM
|
||||||
|
baseAPIKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLLM(mgr *agentAPI.ProviderManager, cfgReg *internalConfig.ConfigRegistry, lua *luaVM.VM, baseAPIKey string) LLMAPI {
|
||||||
|
return &llmImpl{mgr: mgr, cfgReg: cfgReg, lua: lua, baseAPIKey: baseAPIKey}
|
||||||
|
}
|
||||||
|
|
||||||
func (l *llmImpl) ListSources() []string {
|
func (l *llmImpl) ListSources() []string {
|
||||||
if l.mgr == nil { return nil }
|
if l.mgr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return l.mgr.List()
|
return l.mgr.List()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *llmImpl) SetSource(name string) error {
|
func (l *llmImpl) SetSource(name string) error {
|
||||||
if l.mgr == nil { return nil }
|
if l.mgr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return l.mgr.SetDefault(name)
|
return l.mgr.SetDefault(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *llmImpl) CurrentSource() string {
|
func (l *llmImpl) CurrentSource() string {
|
||||||
if l.mgr == nil { return "" }
|
if l.mgr == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
p := l.mgr.Default()
|
p := l.mgr.Default()
|
||||||
if p == nil { return "" }
|
if p == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return p.Name()
|
return p.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *llmImpl) Chat(ctx context.Context, req *LLMCompletionRequest) (*LLMCompletionResponse, error) {
|
||||||
|
if l.mgr == nil {
|
||||||
|
return nil, fmt.Errorf("llm: provider manager not available")
|
||||||
|
}
|
||||||
|
p := l.mgr.Default()
|
||||||
|
if p == nil {
|
||||||
|
return nil, fmt.Errorf("llm: no default provider")
|
||||||
|
}
|
||||||
|
apiReq := &agentAPI.CompletionRequest{
|
||||||
|
Model: req.Model,
|
||||||
|
Temperature: req.Temperature,
|
||||||
|
MaxTokens: req.MaxTokens,
|
||||||
|
Stream: req.Stream,
|
||||||
|
Tools: req.Tools,
|
||||||
|
ToolChoice: req.ToolChoice,
|
||||||
|
DisableThinking: req.DisableThinking,
|
||||||
|
}
|
||||||
|
if len(req.Messages) > 0 {
|
||||||
|
apiReq.Messages = make([]agentAPI.Message, len(req.Messages))
|
||||||
|
for i, m := range req.Messages {
|
||||||
|
msg := agentAPI.Message{
|
||||||
|
Role: m.Role,
|
||||||
|
Content: m.Content,
|
||||||
|
ReasoningContent: m.ReasoningContent,
|
||||||
|
ToolCallID: m.ToolCallID,
|
||||||
|
}
|
||||||
|
if len(m.ToolCalls) > 0 {
|
||||||
|
msg.ToolCalls = make([]agentAPI.ToolCall, len(m.ToolCalls))
|
||||||
|
for j, tc := range m.ToolCalls {
|
||||||
|
msg.ToolCalls[j] = agentAPI.ToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiReq.Messages[i] = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := p.Chat(ctx, apiReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := &LLMCompletionResponse{
|
||||||
|
Content: resp.Content,
|
||||||
|
ReasoningContent: resp.ReasoningContent,
|
||||||
|
FinishReason: resp.FinishReason,
|
||||||
|
TokenUsage: LLMTokenUsage{
|
||||||
|
Prompt: resp.TokenUsage.Prompt,
|
||||||
|
Completion: resp.TokenUsage.Completion,
|
||||||
|
Total: resp.TokenUsage.Total,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range resp.ToolCalls {
|
||||||
|
out.ToolCalls = append(out.ToolCalls, LLMToolCall{ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *llmImpl) ReloadFromConfig() error {
|
||||||
|
if l.mgr == nil || l.cfgReg == nil || l.lua == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cfg := l.cfgReg.ToConfig()
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
l.mgr.Reset()
|
||||||
|
for _, src := range cfg.LLM.Sources {
|
||||||
|
key := src.APIKey
|
||||||
|
if key == "" {
|
||||||
|
key = l.baseAPIKey
|
||||||
|
}
|
||||||
|
provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
|
||||||
|
Model: src.Model,
|
||||||
|
BaseURL: src.BaseURL,
|
||||||
|
APIKey: key,
|
||||||
|
Temperature: cfg.LLM.Temperature,
|
||||||
|
MaxTokens: cfg.LLM.MaxTokens,
|
||||||
|
ContextWindow: src.ContextWindow,
|
||||||
|
}, l.lua, src.Name, src.Adapter)
|
||||||
|
l.mgr.Register(src.Name, provider)
|
||||||
|
}
|
||||||
|
if cfg.LLM.Provider != "" {
|
||||||
|
_ = l.mgr.SetDefault(cfg.LLM.Provider)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ LLMAPI = (*llmImpl)(nil)
|
var _ LLMAPI = (*llmImpl)(nil)
|
||||||
|
|||||||
@ -2,12 +2,26 @@ package sdk
|
|||||||
|
|
||||||
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||||
|
|
||||||
type MemoryAPI = pubsdk.MemoryAPI
|
// MemoryAPI 是内置插件使用的全量图记忆接口。
|
||||||
|
type MemoryAPI interface {
|
||||||
|
pubsdk.MemoryAPI
|
||||||
|
// GraphData 返回整个知识图谱的完整快照。
|
||||||
|
GraphData() (map[string]interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Entity = pubsdk.Entity
|
type Entity = pubsdk.Entity
|
||||||
type Relation = pubsdk.Relation
|
type Relation = pubsdk.Relation
|
||||||
type Triple = pubsdk.Triple
|
type Triple = pubsdk.Triple
|
||||||
|
|
||||||
type TextMemoryAPI = pubsdk.TextMemoryAPI
|
// TextMemoryAPI 是内置插件使用的全量文本记忆接口。
|
||||||
|
type TextMemoryAPI interface {
|
||||||
|
pubsdk.TextMemoryAPI
|
||||||
|
// RecentEvents 返回最近 n 条记忆事件。
|
||||||
|
RecentEvents(n int) ([]TextEvent, error)
|
||||||
|
// Stats 返回文本记忆的运行统计。
|
||||||
|
Stats() map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
type TextEvent = pubsdk.TextEvent
|
type TextEvent = pubsdk.TextEvent
|
||||||
|
|
||||||
type DocMemoryAPI = pubsdk.DocMemoryAPI
|
type DocMemoryAPI = pubsdk.DocMemoryAPI
|
||||||
|
|||||||
@ -50,6 +50,11 @@ func (m *graphMemory) Purge(criteria map[string]string, mode string) (int, error
|
|||||||
return m.db.Purge(criteria, mode)
|
return m.db.Purge(criteria, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *graphMemory) GraphData() (map[string]interface{}, error) {
|
||||||
|
if m.db == nil { return map[string]interface{}{}, nil }
|
||||||
|
return m.db.GraphData()
|
||||||
|
}
|
||||||
|
|
||||||
type textMemoryImpl struct{ tm *text.Memory }
|
type textMemoryImpl struct{ tm *text.Memory }
|
||||||
|
|
||||||
func NewTextMemory(tm *text.Memory) TextMemoryAPI { return &textMemoryImpl{tm: tm} }
|
func NewTextMemory(tm *text.Memory) TextMemoryAPI { return &textMemoryImpl{tm: tm} }
|
||||||
@ -61,6 +66,22 @@ func (m *textMemoryImpl) Append(evt TextEvent) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *textMemoryImpl) RecentEvents(n int) ([]TextEvent, error) {
|
||||||
|
if m.tm == nil { return nil, nil }
|
||||||
|
got, err := m.tm.RecentEvents(n)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
out := make([]TextEvent, len(got))
|
||||||
|
for i, e := range got {
|
||||||
|
out[i] = TextEvent{Role: e.Source, Content: e.Input, Timestamp: e.Timestamp, Channel: e.AgentID}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *textMemoryImpl) Stats() map[string]interface{} {
|
||||||
|
if m.tm == nil { return map[string]interface{}{} }
|
||||||
|
return m.tm.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
type docMemoryImpl struct{ ds *doc.Store }
|
type docMemoryImpl struct{ ds *doc.Store }
|
||||||
|
|
||||||
func NewDocMemory(ds *doc.Store) DocMemoryAPI { return &docMemoryImpl{ds: ds} }
|
func NewDocMemory(ds *doc.Store) DocMemoryAPI { return &docMemoryImpl{ds: ds} }
|
||||||
|
|||||||
@ -57,6 +57,12 @@ type DisabledPluginInfo struct {
|
|||||||
DisabledBy string `json:"disabled_by"`
|
DisabledBy string `json:"disabled_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PluginMeta is the display-name metadata for a plugin (from plg.json / RegisterPluginMeta).
|
||||||
|
type PluginMeta struct {
|
||||||
|
NameZh string `json:"name_zh"`
|
||||||
|
NameEn string `json:"name_en"`
|
||||||
|
}
|
||||||
|
|
||||||
type PluginManager interface {
|
type PluginManager interface {
|
||||||
ListLoadedPlugins() []string
|
ListLoadedPlugins() []string
|
||||||
ListDisabledPlugins() []DisabledPluginInfo
|
ListDisabledPlugins() []DisabledPluginInfo
|
||||||
@ -64,18 +70,45 @@ type PluginManager interface {
|
|||||||
DisablePlugin(name, by string) error
|
DisablePlugin(name, by string) error
|
||||||
EnablePlugin(name string) error
|
EnablePlugin(name string) error
|
||||||
ReloadPlugins() (string, error)
|
ReloadPlugins() (string, error)
|
||||||
|
PluginMetas() map[string]PluginMeta
|
||||||
|
PluginDir() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginSDK struct {
|
type PluginSDK struct {
|
||||||
*pubsdk.PluginSDK
|
*pubsdk.PluginSDK
|
||||||
|
settings SettingsAPI
|
||||||
|
memory MemoryAPI
|
||||||
|
textMem TextMemoryAPI
|
||||||
|
docMem DocMemoryAPI
|
||||||
|
know KnowledgeAPI
|
||||||
|
llm LLMAPI
|
||||||
|
|
||||||
iom *agentIO.IOManager
|
iom *agentIO.IOManager
|
||||||
eventBus *events.Bus
|
eventBus *events.Bus
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
pluginMgr PluginManager
|
pluginMgr PluginManager
|
||||||
|
|
||||||
|
status StatusAPI
|
||||||
|
supervisor SupervisorAPI
|
||||||
|
skill SkillAPI
|
||||||
|
adapter AdapterAPI
|
||||||
|
tracker TrackerAPI
|
||||||
|
config ConfigAPI
|
||||||
|
tool ToolAPI
|
||||||
|
indexer IndexerAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSDK) PluginMgr() PluginManager { return s.pluginMgr }
|
func (s *PluginSDK) PluginMgr() PluginManager { return s.pluginMgr }
|
||||||
|
|
||||||
|
// 以下访问器遮蔽公共 SDK 的同名方法,返回内置插件可用的全量接口。
|
||||||
|
|
||||||
|
func (s *PluginSDK) Settings() SettingsAPI { return s.settings }
|
||||||
|
func (s *PluginSDK) Memory() MemoryAPI { return s.memory }
|
||||||
|
func (s *PluginSDK) TextMemory() TextMemoryAPI { return s.textMem }
|
||||||
|
func (s *PluginSDK) DocMemory() DocMemoryAPI { return s.docMem }
|
||||||
|
func (s *PluginSDK) Knowledge() KnowledgeAPI { return s.know }
|
||||||
|
func (s *PluginSDK) LLM() LLMAPI { return s.llm }
|
||||||
|
|
||||||
// ioAdapter 桥接 IOManager 到公共 SDK 的 IOInjector 接口,
|
// ioAdapter 桥接 IOManager 到公共 SDK 的 IOInjector 接口,
|
||||||
// 确保外部插件通过 s.InjectText() 等方法的调用能被路由到内核 IO 层。
|
// 确保外部插件通过 s.InjectText() 等方法的调用能被路由到内核 IO 层。
|
||||||
type ioAdapter struct{ iom *agentIO.IOManager }
|
type ioAdapter struct{ iom *agentIO.IOManager }
|
||||||
@ -114,6 +147,15 @@ type SDKConfig struct {
|
|||||||
RegOutput OutputChannelRegistrar
|
RegOutput OutputChannelRegistrar
|
||||||
RegInput InputChannelRegistrar
|
RegInput InputChannelRegistrar
|
||||||
PluginMgr PluginManager
|
PluginMgr PluginManager
|
||||||
|
|
||||||
|
Status StatusAPI
|
||||||
|
Supervisor SupervisorAPI
|
||||||
|
Skill SkillAPI
|
||||||
|
Adapter AdapterAPI
|
||||||
|
Tracker TrackerAPI
|
||||||
|
Config ConfigAPI
|
||||||
|
Tool ToolAPI
|
||||||
|
Indexer IndexerAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(name string, cfg SDKConfig) *PluginSDK {
|
func New(name string, cfg SDKConfig) *PluginSDK {
|
||||||
@ -131,13 +173,38 @@ func New(name string, cfg SDKConfig) *PluginSDK {
|
|||||||
base.SetLLMAPI(cfg.LLM)
|
base.SetLLMAPI(cfg.LLM)
|
||||||
return &PluginSDK{
|
return &PluginSDK{
|
||||||
PluginSDK: base,
|
PluginSDK: base,
|
||||||
|
settings: cfg.Settings,
|
||||||
|
memory: cfg.Memory,
|
||||||
|
textMem: cfg.TextMemory,
|
||||||
|
docMem: cfg.DocMemory,
|
||||||
|
know: cfg.Knowledge,
|
||||||
|
llm: cfg.LLM,
|
||||||
|
|
||||||
iom: cfg.IOManager,
|
iom: cfg.IOManager,
|
||||||
eventBus: cfg.EventBus,
|
eventBus: cfg.EventBus,
|
||||||
logger: log.Default(),
|
logger: log.Default(),
|
||||||
pluginMgr: cfg.PluginMgr,
|
pluginMgr: cfg.PluginMgr,
|
||||||
|
|
||||||
|
status: cfg.Status,
|
||||||
|
supervisor: cfg.Supervisor,
|
||||||
|
skill: cfg.Skill,
|
||||||
|
adapter: cfg.Adapter,
|
||||||
|
tracker: cfg.Tracker,
|
||||||
|
config: cfg.Config,
|
||||||
|
tool: cfg.Tool,
|
||||||
|
indexer: cfg.Indexer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PluginSDK) Status() StatusAPI { return s.status }
|
||||||
|
func (s *PluginSDK) Supervisor() SupervisorAPI { return s.supervisor }
|
||||||
|
func (s *PluginSDK) Skill() SkillAPI { return s.skill }
|
||||||
|
func (s *PluginSDK) Adapter() AdapterAPI { return s.adapter }
|
||||||
|
func (s *PluginSDK) Tracker() TrackerAPI { return s.tracker }
|
||||||
|
func (s *PluginSDK) Config() ConfigAPI { return s.config }
|
||||||
|
func (s *PluginSDK) Tool() ToolAPI { return s.tool }
|
||||||
|
func (s *PluginSDK) Indexer() IndexerAPI { return s.indexer }
|
||||||
|
|
||||||
func (s *PluginSDK) InjectInput(source, channel, eventType string, payload map[string]interface{}) {
|
func (s *PluginSDK) InjectInput(source, channel, eventType string, payload map[string]interface{}) {
|
||||||
if s.iom != nil {
|
if s.iom != nil {
|
||||||
s.iom.InjectInputTo(source, channel, eventType, payload)
|
s.iom.InjectInputTo(source, channel, eventType, payload)
|
||||||
|
|||||||
@ -2,5 +2,13 @@ package sdk
|
|||||||
|
|
||||||
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
import pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||||
|
|
||||||
type SettingsAPI = pubsdk.SettingsAPI
|
// SettingsAPI 是内置插件使用的全量配置接口。
|
||||||
|
type SettingsAPI interface {
|
||||||
|
pubsdk.SettingsAPI
|
||||||
|
// DefsCore 返回核心配置表中匹配前缀的配置定义。
|
||||||
|
DefsCore(prefix string) []*ConfigDef
|
||||||
|
// DefsPlugin 返回另一个插件的配置定义。
|
||||||
|
DefsPlugin(plugin, prefix string) []*ConfigDef
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigDef = pubsdk.ConfigDef
|
type ConfigDef = pubsdk.ConfigDef
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func (s *settingsImpl) Defs(prefix string) []*ConfigDef {
|
|||||||
// ConfigDef = pubsdk.ConfigDef (type alias), so direct conversion works
|
// ConfigDef = pubsdk.ConfigDef (type alias), so direct conversion works
|
||||||
cpy := ConfigDef{
|
cpy := ConfigDef{
|
||||||
Key: d.Key, Type: d.Type, DisplayName: d.DisplayName, Description: d.Description,
|
Key: d.Key, Type: d.Type, DisplayName: d.DisplayName, Description: d.Description,
|
||||||
Category: d.Category, Options: d.Options,
|
Category: d.Category, Options: d.Options, Default: d.Default,
|
||||||
}
|
}
|
||||||
out[i] = &cpy
|
out[i] = &cpy
|
||||||
}
|
}
|
||||||
@ -97,6 +97,25 @@ func (s *settingsImpl) Plugins() []string {
|
|||||||
result = append(result, names...)
|
result = append(result, names...)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
func (s *settingsImpl) DefsCore(prefix string) []*ConfigDef {
|
||||||
|
if s.reg == nil { return nil }
|
||||||
|
return mapDefs(s.reg.ListDefs(prefix))
|
||||||
|
}
|
||||||
|
func (s *settingsImpl) DefsPlugin(plugin, prefix string) []*ConfigDef {
|
||||||
|
if s.reg == nil { return nil }
|
||||||
|
return mapDefs(s.reg.PluginConfig(plugin).ListDefs(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapDefs(defs []*internalConfig.ConfigDef) []*ConfigDef {
|
||||||
|
out := make([]*ConfigDef, len(defs))
|
||||||
|
for i, d := range defs {
|
||||||
|
out[i] = &ConfigDef{
|
||||||
|
Key: d.Key, Type: d.Type, DisplayName: d.DisplayName, Description: d.Description,
|
||||||
|
Category: d.Category, Options: d.Options, Default: d.Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func stringifyDefault(v interface{}) string {
|
func stringifyDefault(v interface{}) string {
|
||||||
if v == nil { return "" }
|
if v == nil { return "" }
|
||||||
|
|||||||
22
internal/sdk/skill.go
Normal file
22
internal/sdk/skill.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
// SkillAPI provides access to the skill manager.
|
||||||
|
type SkillAPI interface {
|
||||||
|
List() []*Skill
|
||||||
|
Get(name string) *Skill
|
||||||
|
Install(name, content string) error
|
||||||
|
Uninstall(name string) error
|
||||||
|
Toggle(name string, enabled bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill is a neutral description of an installed skill.
|
||||||
|
type Skill struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Author string `json:"author,omitempty"`
|
||||||
|
Entry string `json:"entry,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
RawContent string `json:"-"`
|
||||||
|
}
|
||||||
99
internal/sdk/status.go
Normal file
99
internal/sdk/status.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
// StatusAPI provides a snapshot of the kernel runtime status.
|
||||||
|
type StatusAPI interface {
|
||||||
|
GetKernelStatus() *KernelStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// KernelStatus is the aggregated runtime snapshot of all kernel subsystems.
|
||||||
|
type KernelStatus struct {
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
StartTime string `json:"start_time"`
|
||||||
|
|
||||||
|
AgentID string `json:"agent_id"`
|
||||||
|
|
||||||
|
Plugins []PluginInfo `json:"plugins"`
|
||||||
|
Tools []ToolDef `json:"tools"`
|
||||||
|
Channels []ChannelInfo `json:"channels"`
|
||||||
|
|
||||||
|
Memory MemoryStatus `json:"memory"`
|
||||||
|
Knowledge KnowledgeStatus `json:"knowledge"`
|
||||||
|
Documents DocumentStatus `json:"documents"`
|
||||||
|
TextMemory TextMemoryStatus `json:"text_memory"`
|
||||||
|
Social SocialStatus `json:"social"`
|
||||||
|
Skills SkillsStatus `json:"skills"`
|
||||||
|
|
||||||
|
LLM LLMStatus `json:"llm"`
|
||||||
|
|
||||||
|
Context ContextStatus `json:"context"`
|
||||||
|
|
||||||
|
Runtime RuntimeStatus `json:"runtime"`
|
||||||
|
|
||||||
|
Tracker TrackerStatus `json:"tracker"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Loaded bool `json:"loaded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
EntityCount int `json:"entity_count"`
|
||||||
|
RelationCount int `json:"relation_count"`
|
||||||
|
EntityTypes int `json:"entity_types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KnowledgeStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
ItemCount int `json:"item_count"`
|
||||||
|
Items []string `json:"items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
DocCount int `json:"doc_count"`
|
||||||
|
VectorCount int `json:"vector_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextMemoryStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
FileCount int `json:"file_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocialStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
PersonCount int `json:"person_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkillsStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
SkillList []string `json:"skill_list,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LLMStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Sources int `json:"sources,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextStatus struct {
|
||||||
|
EventCount int `json:"event_count,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuntimeStatus struct {
|
||||||
|
Goroutines int `json:"goroutines"`
|
||||||
|
MemoryMB int64 `json:"memory_mb"`
|
||||||
|
GoVersion string `json:"go_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackerStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Dir string `json:"dir,omitempty"`
|
||||||
|
}
|
||||||
25
internal/sdk/supervisor.go
Normal file
25
internal/sdk/supervisor.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SupervisorAPI provides access to agent lifecycle management.
|
||||||
|
type SupervisorAPI interface {
|
||||||
|
ListAgents() []AgentStatus
|
||||||
|
GetAgentStatus(id string) (*AgentStatus, error)
|
||||||
|
PreActionSnapshot(id string) (*types.Snapshot, error)
|
||||||
|
RollbackAgent(id string, snapID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentStatus is a neutral snapshot of a supervised agent.
|
||||||
|
type AgentStatus struct {
|
||||||
|
ID types.AgentID `json:"id"`
|
||||||
|
State types.AgentState `json:"state"`
|
||||||
|
Health types.HealthStatus `json:"health"`
|
||||||
|
Uptime time.Duration `json:"uptime,omitempty"`
|
||||||
|
Network types.NetworkCheckResult `json:"network,omitempty"`
|
||||||
|
TrackerStats map[string]interface{} `json:"tracker_stats,omitempty"`
|
||||||
|
}
|
||||||
20
internal/sdk/tool.go
Normal file
20
internal/sdk/tool.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
// ToolSource 描述内核工具注册表/执行器的只读视图(由 StageHost 实现)。
|
||||||
|
// 用接口而非具体类型,避免 sdk 依赖内核包(内核包反向依赖 sdk)。
|
||||||
|
type ToolSource interface {
|
||||||
|
GetToolDefs() []ToolDef
|
||||||
|
ToolDef(name string) *ToolDef
|
||||||
|
ExecuteTool(name string, args map[string]interface{}) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolAPI exposes the kernel tool registry and executor
|
||||||
|
// (StageHost for plugin tools + IOManager for device/channel tools).
|
||||||
|
type ToolAPI interface {
|
||||||
|
// GetToolDefs returns all tools registered on the stage host (plugin tools).
|
||||||
|
GetToolDefs() []ToolDef
|
||||||
|
// GetAllTools returns all tools exposed by IO devices/channels.
|
||||||
|
GetAllTools() []ToolDef
|
||||||
|
// ExecuteTool executes a tool by name, resolving across the stage host first.
|
||||||
|
ExecuteTool(name string, args map[string]interface{}) (interface{}, error)
|
||||||
|
}
|
||||||
50
internal/sdk/tool_impl.go
Normal file
50
internal/sdk/tool_impl.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// toolImpl 桥接 StageHost(插件工具)与 IOManager(设备/通道工具)。
|
||||||
|
type toolImpl struct {
|
||||||
|
stageHost ToolSource
|
||||||
|
iom *agentIO.IOManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTool(sh ToolSource, iom *agentIO.IOManager) ToolAPI {
|
||||||
|
return &toolImpl{stageHost: sh, iom: iom}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *toolImpl) GetToolDefs() []ToolDef {
|
||||||
|
if t.stageHost == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t.stageHost.GetToolDefs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *toolImpl) GetAllTools() []ToolDef {
|
||||||
|
if t.iom == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defs := t.iom.GetAllTools()
|
||||||
|
out := make([]ToolDef, 0, len(defs))
|
||||||
|
for _, d := range defs {
|
||||||
|
out = append(out, ToolDef{Name: d.Name, Description: d.Description, Parameters: d.Parameters})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *toolImpl) ExecuteTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||||
|
if t.stageHost != nil {
|
||||||
|
if def := t.stageHost.ToolDef(name); def != nil {
|
||||||
|
return t.stageHost.ExecuteTool(name, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.iom != nil {
|
||||||
|
return t.iom.ExecuteTool(name, args)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("tool %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ToolAPI = (*toolImpl)(nil)
|
||||||
36
internal/sdk/tracker.go
Normal file
36
internal/sdk/tracker.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// TrackerAPI provides access to the file change tracker / rollback.
|
||||||
|
type TrackerAPI interface {
|
||||||
|
ChangeSets() []*ChangeSet
|
||||||
|
Rollback() error
|
||||||
|
Stats() map[string]interface{}
|
||||||
|
HasChanges() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChangeFileCreated ChangeType = "created"
|
||||||
|
ChangeFileModified ChangeType = "modified"
|
||||||
|
ChangeFileDeleted ChangeType = "deleted"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileChange struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Type ChangeType `json:"type"`
|
||||||
|
SizeBefore int64 `json:"size_before,omitempty"`
|
||||||
|
SizeAfter int64 `json:"size_after,omitempty"`
|
||||||
|
HashBefore string `json:"hash_before,omitempty"`
|
||||||
|
HashAfter string `json:"hash_after,omitempty"`
|
||||||
|
Content []byte `json:"-"` // 回滚用原始内容,不参与 JSON 序列化
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeSet struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Files []FileChange `json:"files"`
|
||||||
|
}
|
||||||
@ -8,18 +8,12 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Skill struct {
|
// Skill 已迁入内置 SDK,此处保留别名以兼容现有调用方。
|
||||||
Name string `json:"name"`
|
type Skill = sdk.Skill
|
||||||
Description string `json:"description"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Author string `json:"author,omitempty"`
|
|
||||||
Entry string `json:"entry,omitempty"`
|
|
||||||
Source string `json:"source,omitempty"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
RawContent string `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@ -195,6 +189,9 @@ func (m *Manager) GetInjectedPrompt() string {
|
|||||||
return strings.Join(parts, "\n\n")
|
return strings.Join(parts, "\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manager 直接满足内置 SDK 的 SkillAPI(复用优先,无需独立适配器)。
|
||||||
|
var _ sdk.SkillAPI = (*Manager)(nil)
|
||||||
|
|
||||||
func extractDescription(content string) string {
|
func extractDescription(content string) string {
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/network"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/network"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||||||
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -224,11 +225,5 @@ func (d *Daemon) RollbackAgent(id types.AgentID, snapID types.SnapshotID) error
|
|||||||
return fmt.Errorf("no tracker available for rollback")
|
return fmt.Errorf("no tracker available for rollback")
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentStatus struct {
|
// AgentStatus 已迁入内置 SDK,此处保留别名以兼容现有调用方。
|
||||||
ID types.AgentID `json:"id"`
|
type AgentStatus = sdk.AgentStatus
|
||||||
State types.AgentState `json:"state"`
|
|
||||||
Health types.HealthStatus `json:"health"`
|
|
||||||
Uptime time.Duration `json:"uptime,omitempty"`
|
|
||||||
Network types.NetworkCheckResult `json:"network,omitempty"`
|
|
||||||
TrackerStats map[string]interface{} `json:"tracker_stats,omitempty"`
|
|
||||||
}
|
|
||||||
|
|||||||
48
internal/supervisor/sdk_adapter.go
Normal file
48
internal/supervisor/sdk_adapter.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package supervisor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sdkAdapter 实现 sdk.SupervisorAPI,将字符串签名的中立接口桥接到
|
||||||
|
// Daemon 的强类型签名(types.AgentID / types.SnapshotID)。
|
||||||
|
type sdkAdapter struct {
|
||||||
|
d *Daemon
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSDKAdapter(d *Daemon) sdk.SupervisorAPI {
|
||||||
|
return &sdkAdapter{d: d}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *sdkAdapter) ListAgents() []sdk.AgentStatus {
|
||||||
|
if a.d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.d.ListAgents()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *sdkAdapter) GetAgentStatus(id string) (*sdk.AgentStatus, error) {
|
||||||
|
if a.d == nil {
|
||||||
|
return nil, errors.New("supervisor not available")
|
||||||
|
}
|
||||||
|
return a.d.GetAgentStatus(types.AgentID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *sdkAdapter) PreActionSnapshot(id string) (*types.Snapshot, error) {
|
||||||
|
if a.d == nil {
|
||||||
|
return nil, errors.New("supervisor not available")
|
||||||
|
}
|
||||||
|
return a.d.PreActionSnapshot(types.AgentID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *sdkAdapter) RollbackAgent(id string, snapID string) error {
|
||||||
|
if a.d == nil {
|
||||||
|
return errors.New("supervisor not available")
|
||||||
|
}
|
||||||
|
return a.d.RollbackAgent(types.AgentID(id), types.SnapshotID(snapID))
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ sdk.SupervisorAPI = (*sdkAdapter)(nil)
|
||||||
@ -7,32 +7,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChangeType string
|
// DTO 已迁入内置 SDK,此处保留别名以兼容现有调用方。
|
||||||
|
type ChangeType = sdk.ChangeType
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChangeFileCreated ChangeType = "created"
|
ChangeFileCreated = sdk.ChangeFileCreated
|
||||||
ChangeFileModified ChangeType = "modified"
|
ChangeFileModified = sdk.ChangeFileModified
|
||||||
ChangeFileDeleted ChangeType = "deleted"
|
ChangeFileDeleted = sdk.ChangeFileDeleted
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileChange struct {
|
type FileChange = sdk.FileChange
|
||||||
Path string `json:"path"`
|
type ChangeSet = sdk.ChangeSet
|
||||||
Type ChangeType `json:"type"`
|
|
||||||
SizeBefore int64 `json:"size_before,omitempty"`
|
|
||||||
SizeAfter int64 `json:"size_after,omitempty"`
|
|
||||||
HashBefore string `json:"hash_before,omitempty"`
|
|
||||||
HashAfter string `json:"hash_after,omitempty"`
|
|
||||||
Content []byte `json:"-"` // stored separately, not in JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangeSet struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
Files []FileChange `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChangeSet(action string) *ChangeSet {
|
func NewChangeSet(action string) *ChangeSet {
|
||||||
return &ChangeSet{
|
return &ChangeSet{
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tracker struct {
|
type Tracker struct {
|
||||||
@ -302,3 +304,6 @@ func (t *Tracker) Stats() map[string]interface{} {
|
|||||||
"max_changeset_age": t.maxChangesetAge.String(),
|
"max_changeset_age": t.maxChangesetAge.String(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tracker 直接满足内置 SDK 的 TrackerAPI(复用优先,无需独立适配器)。
|
||||||
|
var _ sdk.TrackerAPI = (*Tracker)(nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user