diff --git a/internal/plugins/clawhubadapter/manager/main.js b/internal/plugins/clawhubadapter/manager/main.js index e379eb4..59b4e01 100644 --- a/internal/plugins/clawhubadapter/manager/main.js +++ b/internal/plugins/clawhubadapter/manager/main.js @@ -176,7 +176,26 @@ function loadPlugin(pluginDir, name) { if (p && p.id) allProviders['llm'] = { name: p.id, instance: p }; 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 } }), 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 } }), @@ -636,6 +655,39 @@ rl.on('line', async (line) => { const toolName = params.name; 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); if (!tool) { sendError(id, -32601, `Tool not found: ${toolName}`); return; } if (typeof tool.execute !== 'function') { sendError(id, -32603, `Tool ${toolName} has no execute`); return; } diff --git a/internal/plugins/clawhubadapter/plugin.go b/internal/plugins/clawhubadapter/plugin.go index 28c8ddf..6dd3d2a 100644 --- a/internal/plugins/clawhubadapter/plugin.go +++ b/internal/plugins/clawhubadapter/plugin.go @@ -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) { + 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" { return } diff --git a/internal/plugins/clawhubadapter/registry.go b/internal/plugins/clawhubadapter/registry.go index 7adff38..5ffab8d 100644 --- a/internal/plugins/clawhubadapter/registry.go +++ b/internal/plugins/clawhubadapter/registry.go @@ -4,12 +4,14 @@ import ( "encoding/json" "fmt" "log" + "strings" "sync" - agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) +var channelInputBuf = map[string][]map[string]interface{}{} + type ToolRegistry struct{} 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) } -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{} 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 == "" { return } - dev := &channelDevice{ - name: d.Name, - pluginName: pluginName, - sp: sp, - ocType: d.Type, - } - if err := s.RegisterChannel(d.Name, dev); err != nil { - log.Printf("[clawhubadapter] register channel %s: %v", d.Name, err) + chName := d.Name + + 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, 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{}