mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
- manager/main.js:makeChannelRuntime(dispatchReplyWithBufferedBlockDispatcher 入站 + deliver 出站 绑定 + typingCallbacks)、startChannels/stopChannels(gateway.startAccount fire-and-forget 生命周期 桥、listAccountIds/resolveAccount 规范签名 cfg 传参)、tools/call 通道分支无 outbound 走 deliver 事件式发送、SIGTERM 优雅停靠、console 输出重定向 stderr 防 JSON-RPC 流污染 - plugin.go:channel_input 改 InjectInputSync 同步注入取回复并经 CallTool 回发通道(修复 InjectInterruptText 无 ResponseCh 致回复静默丢弃);channel_status/channel_output 通知接入 - registry.go:channelStatus 状态缓存 - SDK:公共 IOInjector 增加 InjectInputSync(source, channel, text) string + ioAdapter 实现 - 生产验证:微信发消息 → pollLoop → dispatchReply → mock LLM 回文本 → deliver → ilink/bot/sendmessage status=200 送达
324 lines
11 KiB
Go
324 lines
11 KiB
Go
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)
|
||
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()
|
||
}
|