mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
feat(clawhubadapter): bridge OC channels to HomeAgent output/input channels
- registry.go: replace channelDevice+RegisterChannel with RegisterOutputChannel
handler routes output via sp.CallTool to JS manager outbound handlers
register {plugin}_read_{ch}_input tool for agent to poll buffered input
- plugin.go: handle channel_input notifications in translateAndRegister
buffer payload in channelInputBuf + InjectInterruptText to notify agent
- manager/main.js: registerChannel supports {plugin: ChannelPlugin} format
tools/call routes channel names to outbound.sendText/sendMedia
submitInput sends channel_input notification to Go side
This commit is contained in:
@ -176,7 +176,26 @@ function loadPlugin(pluginDir, name) {
|
|||||||
if (p && p.id) allProviders['llm'] = { name: p.id, instance: p };
|
if (p && p.id) allProviders['llm'] = { name: p.id, instance: p };
|
||||||
notify('register', { type: 'provider', data: { name: p?.id || p?.name } });
|
notify('register', { type: 'provider', data: { name: p?.id || p?.name } });
|
||||||
},
|
},
|
||||||
registerChannel: (ch) => notify('register', { type: 'channel', data: { name: ch.name, type: ch.type } }),
|
registerChannel: (ch) => {
|
||||||
|
let chName = ch.name;
|
||||||
|
let chType = ch.type || 'text';
|
||||||
|
const chPlugin = ch.plugin;
|
||||||
|
|
||||||
|
// OpenClaw ChannelPlugin 格式: { plugin: { id, outbound: { sendText, sendMedia }, ... } }
|
||||||
|
if (chPlugin && typeof chPlugin === 'object') {
|
||||||
|
chName = chName || chPlugin.id || chPlugin.meta?.id || name + '-channel';
|
||||||
|
chType = chType || (chPlugin.capabilities?.media ? 'io' : 'text');
|
||||||
|
registeredChannels[chName] = { pluginName: name, channelPlugin: chPlugin, type: chType };
|
||||||
|
} else {
|
||||||
|
// 简单格式: { name, type, output }
|
||||||
|
registeredChannels[chName] = { pluginName: name, output: ch.output || ch.send, type: chType };
|
||||||
|
}
|
||||||
|
|
||||||
|
notify('register', { type: 'channel', data: { name: chName, type: chType, id: chPlugin?.id } });
|
||||||
|
},
|
||||||
|
submitInput: (msg) => {
|
||||||
|
notify('channel_input', { channel: name, payload: msg });
|
||||||
|
},
|
||||||
registerHook: (hook) => notify('register', { type: 'hook', data: { name: hook.name, event: hook.event } }),
|
registerHook: (hook) => notify('register', { type: 'hook', data: { name: hook.name, event: hook.event } }),
|
||||||
registerHttpRoute: (route) => notify('register', { type: 'http_route', data: { path: route.path, method: route.method } }),
|
registerHttpRoute: (route) => notify('register', { type: 'http_route', data: { path: route.path, method: route.method } }),
|
||||||
registerCommand: (cmd) => notify('register', { type: 'command', data: { name: cmd.name, description: cmd.description } }),
|
registerCommand: (cmd) => notify('register', { type: 'command', data: { name: cmd.name, description: cmd.description } }),
|
||||||
@ -636,6 +655,39 @@ rl.on('line', async (line) => {
|
|||||||
const toolName = params.name;
|
const toolName = params.name;
|
||||||
const args = params.arguments || {};
|
const args = params.arguments || {};
|
||||||
|
|
||||||
|
// 通道输出路由:toolName 匹配已注册通道名时,调通道的输出 handler
|
||||||
|
const ch = registeredChannels[toolName];
|
||||||
|
if (ch) {
|
||||||
|
try {
|
||||||
|
const channelPlugin = ch.channelPlugin;
|
||||||
|
if (channelPlugin && channelPlugin.outbound) {
|
||||||
|
const meta = args.meta || '';
|
||||||
|
let metaObj = {};
|
||||||
|
try { metaObj = typeof meta === 'string' ? JSON.parse(meta) : meta; } catch {}
|
||||||
|
const to = metaObj.user_id || metaObj.to || metaObj.group_id || '';
|
||||||
|
const ctx = { to, text: args.payload || '', mediaUrl: metaObj.mediaUrl || '', cfg: {}, accountId: metaObj.accountId || null };
|
||||||
|
let result;
|
||||||
|
if (ctx.mediaUrl && channelPlugin.outbound.sendMedia) {
|
||||||
|
result = await channelPlugin.outbound.sendMedia(ctx);
|
||||||
|
} else if (channelPlugin.outbound.sendText) {
|
||||||
|
result = await channelPlugin.outbound.sendText(ctx);
|
||||||
|
} else {
|
||||||
|
throw new Error(`channel ${toolName} has no sendText/sendMedia handler`);
|
||||||
|
}
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
|
||||||
|
} else if (typeof ch.output === 'function') {
|
||||||
|
const result = await ch.output(args.payload, args.type, args.meta);
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
|
||||||
|
} else if (typeof ch.send === 'function') {
|
||||||
|
const result = await ch.send(args.payload, args.meta);
|
||||||
|
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
|
||||||
|
} else {
|
||||||
|
sendError(id, -32601, `channel ${toolName} has no output handler`);
|
||||||
|
}
|
||||||
|
} catch (e) { sendError(id, -32603, e.message); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tool = allTools.find(t => t.name === toolName);
|
const tool = allTools.find(t => t.name === toolName);
|
||||||
if (!tool) { sendError(id, -32601, `Tool not found: ${toolName}`); return; }
|
if (!tool) { sendError(id, -32601, `Tool not found: ${toolName}`); return; }
|
||||||
if (typeof tool.execute !== 'function') { sendError(id, -32603, `Tool ${toolName} has no execute`); return; }
|
if (typeof tool.execute !== 'function') { sendError(id, -32603, `Tool ${toolName} has no execute`); return; }
|
||||||
|
|||||||
@ -670,6 +670,23 @@ func (p *Plugin) notifyLoop(sp *sidecarProcess, s *sdk.PluginSDK, pluginName str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) translateAndRegister(n OCNotification, sp *sidecarProcess, s *sdk.PluginSDK, pluginName string) {
|
func (p *Plugin) translateAndRegister(n OCNotification, sp *sidecarProcess, s *sdk.PluginSDK, pluginName string) {
|
||||||
|
if n.Method == "channel_input" {
|
||||||
|
var params struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Payload map[string]interface{} `json:"payload"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(n.Params, ¶ms); err != nil || params.Channel == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelInputBuf[params.Channel] = append(channelInputBuf[params.Channel], params.Payload)
|
||||||
|
content, _ := params.Payload["content"].(string)
|
||||||
|
if content == "" {
|
||||||
|
data, _ := json.Marshal(params.Payload)
|
||||||
|
content = string(data)
|
||||||
|
}
|
||||||
|
s.InjectInterruptText(pluginName, params.Channel, fmt.Sprintf("[%s] %s", params.Channel, content))
|
||||||
|
return
|
||||||
|
}
|
||||||
if n.Method != "register" {
|
if n.Method != "register" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var channelInputBuf = map[string][]map[string]interface{}{}
|
||||||
|
|
||||||
type ToolRegistry struct{}
|
type ToolRegistry struct{}
|
||||||
|
|
||||||
func (r *ToolRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
|
func (r *ToolRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
|
||||||
@ -95,39 +97,6 @@ func (r *ProviderRegistry) Dispatch(typeStr string, data json.RawMessage, plugin
|
|||||||
log.Printf("[clawhubadapter] unknown provider type: %s (plugin: %s)", typeStr, pluginName)
|
log.Printf("[clawhubadapter] unknown provider type: %s (plugin: %s)", typeStr, pluginName)
|
||||||
}
|
}
|
||||||
|
|
||||||
type channelDevice struct {
|
|
||||||
name string
|
|
||||||
pluginName string
|
|
||||||
sp *sidecarProcess
|
|
||||||
ocType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *channelDevice) Name() string { return d.name }
|
|
||||||
func (d *channelDevice) Type() agentIO.DeviceType { return agentIO.DeviceIO }
|
|
||||||
func (d *channelDevice) Description() string { return fmt.Sprintf("OC channel %s (from %s)", d.name, d.pluginName) }
|
|
||||||
func (d *channelDevice) OutputCapabilities() agentIO.OutputCapability { return ocTypeToCap(d.ocType) }
|
|
||||||
func (d *channelDevice) Start() error { return nil }
|
|
||||||
func (d *channelDevice) Stop() error { return nil }
|
|
||||||
func (d *channelDevice) Tools() []agentIO.ToolDef { return nil }
|
|
||||||
func (d *channelDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
|
||||||
return d.sp.CallTool(tool, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ocTypeToCap(t string) agentIO.OutputCapability {
|
|
||||||
switch t {
|
|
||||||
case "text":
|
|
||||||
return agentIO.CapText
|
|
||||||
case "file":
|
|
||||||
return agentIO.CapFile
|
|
||||||
case "image":
|
|
||||||
return agentIO.CapImage
|
|
||||||
case "audio":
|
|
||||||
return agentIO.CapAudio
|
|
||||||
default:
|
|
||||||
return agentIO.CapText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelRegistry struct{}
|
type ChannelRegistry struct{}
|
||||||
|
|
||||||
func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
|
func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *sidecarProcess, s *sdk.PluginSDK) {
|
||||||
@ -138,15 +107,44 @@ func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *
|
|||||||
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
|
if err := json.Unmarshal(data, &d); err != nil || d.Name == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dev := &channelDevice{
|
chName := d.Name
|
||||||
name: d.Name,
|
|
||||||
pluginName: pluginName,
|
var caps int
|
||||||
sp: sp,
|
switch d.Type {
|
||||||
ocType: d.Type,
|
case "file":
|
||||||
}
|
caps = 2
|
||||||
if err := s.RegisterChannel(d.Name, dev); err != nil {
|
case "image":
|
||||||
log.Printf("[clawhubadapter] register channel %s: %v", d.Name, err)
|
caps = 4
|
||||||
|
case "audio":
|
||||||
|
caps = 8
|
||||||
|
default:
|
||||||
|
caps = 1
|
||||||
}
|
}
|
||||||
|
desc := fmt.Sprintf("OC channel %s (from %s)", chName, pluginName)
|
||||||
|
s.RegisterOutputChannel(chName, caps, desc, func(args map[string]interface{}) (interface{}, error) {
|
||||||
|
return sp.CallTool(chName, args)
|
||||||
|
})
|
||||||
|
|
||||||
|
readToolName := fmt.Sprintf("%s_read_%s_input", pluginName, 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) {
|
||||||
|
buf := channelInputBuf[chName]
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return map[string]interface{}{"messages": []interface{}{}}, nil
|
||||||
|
}
|
||||||
|
msgs := make([]interface{}, len(buf))
|
||||||
|
for i, m := range buf {
|
||||||
|
msgs[i] = m
|
||||||
|
}
|
||||||
|
channelInputBuf[chName] = nil
|
||||||
|
return map[string]interface{}{"messages": msgs}, nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type StageRegistry struct{}
|
type StageRegistry struct{}
|
||||||
|
|||||||
Reference in New Issue
Block a user