Phase 0.1/1/3/6: 核心生产问题修复

Healthcheck 隔离 (Phase 0.1):
- 新增 internal/sdk/selftest.go: VirtualInstance 完全隔离自检空间
- PluginSDK.Selftest()/SelftestReset() 暴露隔离实例 (含 mutex)
- LLM 自检只读白名单 isSafeReadonlyTool 防写类工具污染生产
- 单测验证: healthcheck 后生产实例内容不变 + 无残留
- 存量清理: 删除 gotest/luatest 残留目录

GraphDB 去重 (Phase 1):
- migrateRelationUnique: 启动自动重建 relations 表加 UNIQUE 约束并去重
- Commit 改为存在性检查, 重复三元组仅刷新 confidence 不重复插入
- 3 个 dedup 单测全绿

配置时长解析 (Phase 3):
- parseDurationExtended 支持 2d/1w/3h 等人类可读单位
- GetDuration 全局生效, 防 2d 静默回退 30m

Agentcli 通知风暴治理 (Phase 6):
- 语义通知: 累积 notify_bytes(2KB) 或间隔 notify_interval(2s) 触发
- 生命周期即时通知: 启动/进程退出/EOF 立即通知
- 可配置 settings, 保留通知机制保证 agent 感知终端存在
- 运维止血: 已杀掉幽灵 PID 3716282 (bash git sparse clone 运行 16h)

Plan.md: 新增设计意图备忘(插件即App/分层记忆), 更新各 Phase 进度
This commit is contained in:
root
2026-08-12 13:51:40 +08:00
parent c19fea2584
commit 9d5a914941
12 changed files with 962 additions and 39 deletions

View File

@ -2,6 +2,7 @@ package sdk
import (
"log"
"sync"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
@ -98,6 +99,9 @@ type PluginSDK struct {
config ConfigAPI
tool ToolAPI
indexer IndexerAPI
selftestMu sync.Mutex
selftest *VirtualInstance
}
func (s *PluginSDK) PluginMgr() PluginManager { return s.pluginMgr }
@ -211,6 +215,42 @@ func New(name string, cfg SDKConfig) *PluginSDK {
}
}
// Selftest 返回一个隔离的虚拟自检实例healthcheck 等内置插件用),
// 完全独立于生产存储,不产生任何污染。首次调用创建,复用已存在实例;
// 每轮自检前调用 SelftestReset 重建以清空上轮测试数据。
func (s *PluginSDK) Selftest(scope string) (*VirtualInstance, error) {
s.selftestMu.Lock()
defer s.selftestMu.Unlock()
if s.selftest == nil {
vi, err := NewVirtualInstance(scope)
if err != nil {
return nil, err
}
s.selftest = vi
}
return s.selftest, nil
}
// SelftestReset 清理并重建隔离自检实例,用于每轮健康检查前重置状态。
func (s *PluginSDK) SelftestReset(scope string) error {
s.selftestMu.Lock()
defer s.selftestMu.Unlock()
if s.selftest == nil {
vi, err := NewVirtualInstance(scope)
if err != nil {
return err
}
s.selftest = vi
return nil
}
vi, err := s.selftest.Reset(scope)
if err != nil {
return err
}
s.selftest = vi
return nil
}
func (s *PluginSDK) Status() StatusAPI { return s.status }
func (s *PluginSDK) Supervisor() SupervisorAPI { return s.supervisor }
func (s *PluginSDK) Adapter() AdapterAPI { return s.adapter }

108
internal/sdk/selftest.go Normal file
View File

@ -0,0 +1,108 @@
package sdk
import (
"fmt"
"os"
"path/filepath"
"sync"
"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/memory/text"
)
// VirtualInstance 是完全隔离的虚拟存储集合,供内置插件(如 healthcheck
// 做不污染生产存储的"写→查→删"往返自检。所有写入发生在独立临时目录,
// 由 Cleanup 统一销毁。
type VirtualInstance struct {
Memory MemoryAPI
Knowledge KnowledgeAPI
DocMemory DocMemoryAPI
TextMemory TextMemoryAPI
dir string
mu sync.Mutex
created bool
}
// newBaseDir 返回一个隔离的临时根目录hot 前缀,避免与生产数据混淆)。
func newBaseDir(scope string) (string, error) {
return os.MkdirTemp("", "homeagent_selftest_"+scope+"_")
}
// NewVirtualInstance 创建完全隔离的测试实例,使用独立临时目录。
func NewVirtualInstance(scope string) (*VirtualInstance, error) {
if scope == "" {
scope = "hc"
}
base, err := newBaseDir(scope)
if err != nil {
return nil, err
}
v := &VirtualInstance{dir: base, created: true}
if err := v.initLocked(); err != nil {
os.RemoveAll(base)
return nil, err
}
return v, nil
}
// initLocked 初始化各隔离存储。调用方须持 v.mu。
func (v *VirtualInstance) initLocked() error {
memDB, err := memory.NewGraphDB(filepath.Join(v.dir, "graph.db"))
if err != nil {
return fmt.Errorf("virtual graph db: %w", err)
}
v.Memory = NewGraphMemory(memDB)
ks := knowledge.NewStore(filepath.Join(v.dir, "knowledge"))
if err := ks.Start(); err != nil {
return fmt.Errorf("virtual knowledge store: %w", err)
}
v.Knowledge = NewKnowledge(ks)
ds := doc.NewStore(filepath.Join(v.dir, "documents"))
if err := ds.Start(); err != nil {
return fmt.Errorf("virtual doc store: %w", err)
}
v.DocMemory = NewDocMemory(ds)
tm := text.New(filepath.Join(v.dir, "text"))
if err := tm.Start(); err != nil {
return fmt.Errorf("virtual text memory: %w", err)
}
v.TextMemory = NewTextMemory(tm)
return nil
}
// Cleanup 关闭并销毁该虚拟实例的全部临时存储。之后实例不可再用。
func (v *VirtualInstance) Cleanup() {
v.mu.Lock()
defer v.mu.Unlock()
if !v.created {
return
}
if tm, ok := v.TextMemory.(*textMemoryImpl); ok && tm.tm != nil {
tm.tm.Stop()
}
if ds, ok := v.DocMemory.(*docMemoryImpl); ok && ds.ds != nil {
ds.ds.Stop()
}
if ks, ok := v.Knowledge.(*knowledgeImpl); ok && ks.ks != nil {
ks.ks.Stop()
}
if gm, ok := v.Memory.(*graphMemory); ok && gm.db != nil {
gm.db.Close()
}
os.RemoveAll(v.dir)
v.created = false
}
// Reset 清理当前实例并重建一个全新的隔离实例,用于每轮自检前重置状态。
// 返回新实例;失败时旧实例已被清理、返回 err调用方需重新创建。
func (v *VirtualInstance) Reset(scope string) (*VirtualInstance, error) {
v.Cleanup()
return NewVirtualInstance(scope)
}