Files
HomeAgent/internal/plugins/clawhubadapter/registry.go
JianFeeeee 6e6035141a fix(resident/plugin): 二进制级压测暴露的三个真问题(DataDir 漏接线 / inputch 未登记 / create 不开工)+ 通道双向登记贯穿全部内建插件
在真实内核二进制(私有 netns + mock LLM + CLI unix socket)上做压力测试时,
下面三个问题**只有跑真二进制才暴露** —— 单元测试里都显式传了参数、没走插件加载,
所以全绿也照样漏。

## ① 根 agent 的 DataDir 没接线 ⇒ 驻留子永远建不出来

现象:模型调用 `resident_agents` 成功,但结果是
`创建驻留子需要 data_dir 或显式 temp_path`。
根因:`cmd/homed/main.go` 构造 AgentConfig 时没有 `DataDir`,
而驻留子的 temp 图库需要 `<data>/residents/<id>/graph.db` 这个锚点。
(单测里 `AgentConfig{DataDir: dir}` 显式给了,所以测不出来。)

修:main.go 接线 `DataDir: cfg.Daemon.DataDir`;并在工具层加**兜底 + 告警** ——
data_dir 为空时从主图库路径反推(`<data>/memory/graph.db` ⇒ `<data>`),
失败才报错。静默失败会让线上表现成"工具能调但永远建不出来"。

## ② 插件通道没登记为 inputch ⇒ "划入 inputch"必然失败

现象:`划入 inputch cli: inputch 未注册`。
根因:`cli` 插件只调 `RegisterOutputChannel("cli", ...)`,却用同一个名字
`InjectTextSync("cli", ...)` 注入输入 —— 内核 inputch 登记表里根本没有它。
(实测审计:内建 6 个插件里只有 0 个登记过入站通道;SDK 示例里只有 qq/weather 是对的。)

修两处:
- **全部内建插件显式登记入站通道**:`cli`/`agentcli`/`timer`/`webui`(+`http`, NoMemory)/
  `clawhubadapter`(每个 OC 通道声明处)/`remotedevice`(`device/<id>` 懒登记,幂等)。
- `registry.go` 把隐式兜底改成**留痕的兼容网**:只有当该名字还没登记为 inputch 时
  才兜底登记,并打日志说明"建议显式 RegisterInputChannel"。
  实测:改完内建插件后,启动日志里兜底告警 **0 次**。

## ③ create 之后子不开工 ⇒ rounds 恒为 0

现象:`[agent] r1 started, waiting for IO interrupts` 之后什么都没有,登记表里 rounds=0。
根因:`TaskPrompt` 只进了子的**系统提示词**,从没作为输入投给子。
修:create 即开工 —— 把任务提示词作为**第一条排队输入**投给子(排队而非中断:
创建是"安排工作",不是"打断它正在做的事")。

## 测试

- `TestResident_InputchTableAutoAndProactive` / `TestLightKernel_TraditionalContextNoTrimming`
  随行为更新:create 会多跑一轮(任务提示词那轮也会写处理表),
  断言改为"以创建时的表长为基线 + 等待新的一轮"。
- 全量 `go test ./...` = 37 包 ok / 0 FAIL;`-race`(agent/plugin/plugins)干净。

## 真实二进制压力测试结果(修复后)

私有 netns 里跑 mock LLM + 内核,用 CLI socket 驱动多并发连接:
- 密集:16 连接×12 输入 + 4 线程×20 次 L4 中断 → **274 任务 executed=274 / rejected=0 / errors=0**,
  峰值排队 15、峰值待处理中断 76;
- 稀疏(中断每 3s 一次,压在排队任务的流式段上)→ **suspended=27 / resumed=27 / preempted=27**;
- 驻留子全链路:父建子(inputchs=["cli"])→ 子开工 → 子 `notify_parent` → 父侧收到
  `interrupt from r1/child/r1`(L3,且父被抢占 suspended/resumed=1);
- 优雅退出:SIGTERM 后驻留子 temp 目录被清除、无残留进程。
2026-09-13 11:38:03 +08:00

327 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package clawhubadapter
import (
"encoding/json"
"fmt"
"log"
"strings"
"sync"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
var (
channelInputBuf = map[string][]map[string]interface{}{}
channelInputBufMu sync.Mutex
channelStatus map[string]map[string]interface{} // 通道运行状态channel_status 通知)
channelStatusMu sync.Mutex
)
type ToolRegistry struct{}
func (r *ToolRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
var d struct {
Name string `json:"name"`
Label string `json:"label"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
Plugin string `json:"plugin"`
}
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
return
}
pn := pluginName
if d.Plugin != "" {
pn = d.Plugin
}
toolName := fmt.Sprintf("%s_%s", pn, d.Name)
tDef := sdk.ToolDef{
Name: toolName,
Description: d.Description,
Parameters: d.Parameters,
}
handler := func(sp *sidecarProcess, ocToolName string) sdk.ToolHandler {
return func(args map[string]interface{}) (interface{}, error) {
return sp.CallTool(ocToolName, args)
}
}(sp, d.Name)
if err := s.RegisterTool(toolName, tDef, handler); err != nil {
log.Printf("[clawhubadapter] register tool %s: %v", toolName, err)
}
}
type providerDef struct {
ocType string
toolSuffix string
desc string
}
var providerMap = []providerDef{
{"image_generation", "generate_image", "根据文本描述生成图片,返回图片 URL"},
{"music_generation", "generate_music", "根据描述生成音乐"},
{"video_generation", "generate_video", "根据描述生成视频"},
{"speech", "synthesize_speech", "将文本合成为语音"},
{"web_search", "web_search", "搜索互联网信息"},
{"web_fetch", "web_fetch", "获取指定网页的内容"},
{"media_understanding", "analyze_media", "分析图片、音频或视频内容"},
{"realtime_transcription", "transcribe_audio", "将音频转写为文字"},
{"realtime_voice", "voice_io", "实时语音输入输出"},
}
type ProviderRegistry struct{}
func (r *ProviderRegistry) Dispatch(typeStr string, data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
var d struct {
Name string `json:"name"`
Description string `json:"description"`
Plugin string `json:"plugin"`
}
json.Unmarshal(data, &d)
pn := pluginName
if d.Plugin != "" {
pn = d.Plugin
}
// Strip _provider suffix: "image_generation_provider" -> "image_generation"
lookupType := typeStr
if lookupType != "provider" {
lookupType = strings.TrimSuffix(lookupType, "_provider")
}
if lookupType == "" {
return
}
for _, p := range providerMap {
if p.ocType == lookupType {
toolName := fmt.Sprintf("%s_%s", pn, p.toolSuffix)
desc := p.desc
if d.Name != "" {
desc = fmt.Sprintf("[%s] %s", d.Name, desc)
}
props := map[string]interface{}{
"prompt": map[string]interface{}{"type": "string", "description": "Prompt for generation or search query"},
}
switch p.ocType {
case "image_generation":
props["prompt"] = map[string]interface{}{"type": "string", "description": "Image description prompt"}
props["size"] = map[string]interface{}{"type": "string", "description": "Image size (e.g. 1024x1024)", "enum": []interface{}{"256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"}}
case "web_search":
props["query"] = map[string]interface{}{"type": "string", "description": "Search query"}
delete(props, "prompt")
case "web_fetch":
props["url"] = map[string]interface{}{"type": "string", "description": "URL to fetch"}
delete(props, "prompt")
case "media_understanding":
props["url"] = map[string]interface{}{"type": "string", "description": "Media URL to analyze"}
props["media_type"] = map[string]interface{}{"type": "string", "description": "Media type", "enum": []interface{}{"image", "audio", "video"}}
case "speech":
props["text"] = map[string]interface{}{"type": "string", "description": "Text to synthesize"}
props["voice"] = map[string]interface{}{"type": "string", "description": "Voice identifier"}
case "music_generation":
props["prompt"] = map[string]interface{}{"type": "string", "description": "Music description prompt"}
props["duration"] = map[string]interface{}{"type": "number", "description": "Duration in seconds"}
case "video_generation":
props["prompt"] = map[string]interface{}{"type": "string", "description": "Video description prompt"}
props["duration"] = map[string]interface{}{"type": "number", "description": "Duration in seconds"}
case "realtime_transcription":
props["audio_url"] = map[string]interface{}{"type": "string", "description": "Audio URL to transcribe"}
case "realtime_voice":
props["text"] = map[string]interface{}{"type": "string", "description": "Text to speak"}
props["voice"] = map[string]interface{}{"type": "string", "description": "Voice identifier"}
}
tDef := sdk.ToolDef{
Name: toolName,
Description: desc,
Parameters: map[string]interface{}{
"type": "object",
"properties": props,
},
}
handler := func(sp *sidecarProcess, ocType string) sdk.ToolHandler {
return func(args map[string]interface{}) (interface{}, error) {
return sp.CallProvider(ocType, args)
}
}(sp, lookupType)
if err := s.RegisterTool(toolName, tDef, handler); err != nil {
log.Printf("[clawhubadapter] register provider tool %s: %v", toolName, err)
}
return
}
}
if lookupType != typeStr {
log.Printf("[clawhubadapter] provider %s -> lookup %s (plugin: %s)", typeStr, lookupType, pluginName)
}
switch lookupType {
case "provider":
log.Printf("[clawhubadapter] generic LLM provider from %s handled natively by HomeAgent", pluginName)
case "embedding", "memory_embedding":
log.Printf("[clawhubadapter] embedding provider %s from %s ignored (HomeAgent uses native embeddings)", lookupType, pluginName)
default:
log.Printf("[clawhubadapter] unknown provider type: %s (plugin: %s)", typeStr, pluginName)
}
}
type ChannelRegistry struct{}
func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
var d struct {
Name string `json:"name"`
Type string `json:"type"`
Plugin string `json:"plugin"`
}
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
return
}
chName := d.Name
pn := pluginName
if d.Plugin != "" {
pn = d.Plugin
}
var caps int
switch d.Type {
case "file":
caps = 2
case "image":
caps = 4
case "audio":
caps = 8
default:
caps = 1
}
desc := fmt.Sprintf("OC channel %s (from %s)", chName, pn)
// 通道名来自外部 OC 配置(动态):声明处**同时**登记 inputch ——
// 该通道既收InjectInputSync 见 plugin.go又发output_send__<通道>)。
_ = s.RegisterInputChannel(chName, sdk.ChannelDef{})
s.RegisterOutputChannel(chName, caps, desc, sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
return sp.CallTool(chName, args)
})
readToolName := fmt.Sprintf("%s_read_%s_input", pn, strings.ReplaceAll(chName, "-", "_"))
s.RegisterTool(readToolName, sdk.ToolDef{
Name: readToolName,
Description: fmt.Sprintf("读取 %s 通道的待处理输入消息", chName),
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
}, func(args map[string]interface{}) (interface{}, error) {
channelInputBufMu.Lock()
buf := channelInputBuf[chName]
if len(buf) == 0 {
channelInputBufMu.Unlock()
return map[string]interface{}{"messages": []interface{}{}}, nil
}
msgs := make([]interface{}, len(buf))
for i, m := range buf {
msgs[i] = m
}
channelInputBuf[chName] = nil
channelInputBufMu.Unlock()
return map[string]interface{}{"messages": msgs}, nil
})
}
type StageRegistry struct{}
func (r *StageRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
var d struct {
Name string `json:"name"`
Event string `json:"event"`
}
json.Unmarshal(data, &d)
log.Printf("[clawhubadapter] hook %s/%s (plugin: %s) — stub: OC hooks need bidirectional bridge",
d.Name, d.Event, pluginName)
}
type CapRecorder struct {
mu sync.Mutex
caps []string
}
func (r *CapRecorder) Record(typeStr string, data json.RawMessage, pluginName string) {
var d struct {
Name string `json:"name"`
Description string `json:"description"`
}
json.Unmarshal(data, &d)
capStr := fmt.Sprintf("[%s] capability: %s", pluginName, typeStr)
if d.Name != "" {
capStr += " (" + d.Name + ")"
}
if d.Description != "" {
capStr += ": " + d.Description
}
r.mu.Lock()
r.caps = append(r.caps, capStr)
r.mu.Unlock()
}
func (r *CapRecorder) Snapshot() []string {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]string, len(r.caps))
copy(out, r.caps)
return out
}
type RegistryDispatcher struct {
toolReg *ToolRegistry
providerReg *ProviderRegistry
channelReg *ChannelRegistry
stageReg *StageRegistry
capRecorder *CapRecorder
}
func NewDispatcher() *RegistryDispatcher {
return &RegistryDispatcher{
toolReg: &ToolRegistry{},
providerReg: &ProviderRegistry{},
channelReg: &ChannelRegistry{},
stageReg: &StageRegistry{},
capRecorder: &CapRecorder{},
}
}
func (d *RegistryDispatcher) Dispatch(typeStr string, data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
switch typeStr {
case "tool":
d.toolReg.Dispatch(data, pluginName, sp, s)
case "channel":
d.channelReg.Dispatch(data, pluginName, sp, s)
case "provider", "image_generation_provider", "music_generation_provider",
"video_generation_provider", "speech_provider",
"realtime_transcription_provider", "realtime_voice_provider",
"media_understanding_provider",
"web_fetch_provider", "web_search_provider",
"embedding_provider", "memory_embedding_provider":
d.providerReg.Dispatch(typeStr, data, pluginName, sp, s)
d.capRecorder.Record(typeStr, data, pluginName)
case "hook", "runtime_lifecycle", "lifecycle",
"agent_event_subscription", "agent_harness",
"session_event", "conversation_binding_resolved",
"interactive_handler",
"cli", "cli_backend", "node_cli_feature",
"command", "http_route", "service",
"gateway_method", "gateway_discovery_service",
"trusted_tool_policy", "tool_metadata",
"context_engine",
"memory_capability", "memory_prompt_section",
"memory_flush_plan", "memory_runtime",
"memory_prompt_supplement", "memory_corpus_supplement",
"session_extension", "session_scheduler_job",
"session_action", "control_ui_descriptor",
"agent_tool_result_middleware":
d.stageReg.Dispatch(data, pluginName, sp, s)
d.capRecorder.Record(typeStr, data, pluginName)
default:
d.capRecorder.Record(typeStr, data, pluginName)
}
}
func (d *RegistryDispatcher) Capabilities() []string {
return d.capRecorder.Snapshot()
}