mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
refactor: remove IO route mapping, add HTTP API tests, system prompt update
This commit is contained in:
@ -198,18 +198,9 @@ func (r *Registry) Register(p Plugin) {
|
||||
return // 纯技能插件,无 IO 通道
|
||||
}
|
||||
|
||||
// 自动注册输出路由
|
||||
if cfg := p.IOConfig(); cfg != nil {
|
||||
log.Printf("[plugin] io device %s active (type=%s, caps=%v)",
|
||||
p.Name(), cfg.Type, cfg.OutputCaps)
|
||||
if cfg.InputRoute != "" {
|
||||
outputRoute := cfg.OutputRoute
|
||||
if outputRoute == "" {
|
||||
outputRoute = cfg.InputRoute
|
||||
}
|
||||
r.ioMgr.RegisterOutputRoute(cfg.InputRoute, outputRoute)
|
||||
log.Printf("[plugin] route: %s → %s", cfg.InputRoute, outputRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -300,7 +291,6 @@ func (r *Registry) Reload(dir string) (string, error) {
|
||||
|
||||
// 2. 启动新设备的 IO 通道
|
||||
newDevices := make(map[string]agentIO.Device)
|
||||
newRoutes := make(map[string]string)
|
||||
for _, lp := range loaded {
|
||||
if lp.err != nil {
|
||||
log.Printf("[plugin] skip %s: %v", lp.name, lp.err)
|
||||
@ -313,15 +303,6 @@ func (r *Registry) Reload(dir string) (string, error) {
|
||||
if dev != nil {
|
||||
dev.Start() // 新设备预先启动
|
||||
newDevices[lp.name] = dev
|
||||
if cfg := lp.p.IOConfig(); cfg != nil {
|
||||
if cfg.InputRoute != "" {
|
||||
out := cfg.OutputRoute
|
||||
if out == "" {
|
||||
out = cfg.InputRoute
|
||||
}
|
||||
newRoutes[cfg.InputRoute] = out
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,7 +311,7 @@ func (r *Registry) Reload(dir string) (string, error) {
|
||||
var oldPlugins map[string]Plugin
|
||||
if r.ioMgr != nil {
|
||||
// 获取旧设备并原子替换
|
||||
oldDevices := r.ioMgr.AtomicSwapDevices(newDevices, newRoutes)
|
||||
oldDevices := r.ioMgr.AtomicSwapDevices(newDevices)
|
||||
// 停止旧设备
|
||||
for _, dev := range oldDevices {
|
||||
go dev.Stop()
|
||||
@ -672,14 +653,8 @@ func (d *PluginDevice) Description() string { return d.plugin.Description(
|
||||
|
||||
func (d *PluginDevice) Tools() []agentIO.ToolDef {
|
||||
pts := d.plugin.Tools()
|
||||
defs := make([]agentIO.ToolDef, 0, len(pts))
|
||||
for _, t := range pts {
|
||||
defs = append(defs, agentIO.ToolDef{
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
Parameters: t.Parameters,
|
||||
})
|
||||
}
|
||||
defs := make([]agentIO.ToolDef, len(pts))
|
||||
copy(defs, pts)
|
||||
return defs
|
||||
}
|
||||
|
||||
@ -713,10 +688,21 @@ func extractDescription(content string) string {
|
||||
// extractField finds `field: value` pattern in content
|
||||
func extractField(content string, field string) string {
|
||||
prefix := field + ":"
|
||||
lowerPrefix := toLower(prefix)
|
||||
for _, line := range splitLines(content) {
|
||||
trimmed := trimSpace(line)
|
||||
if hasPrefix(toLower(trimmed), prefix) {
|
||||
return trimSpace(trimPrefix(trimmed, prefix))
|
||||
if hasPrefix(toLower(trimmed), lowerPrefix) {
|
||||
// 找到冒号位置,提取冒号后的内容
|
||||
colonIdx := -1
|
||||
for i := 0; i < len(trimmed); i++ {
|
||||
if trimmed[i] == ':' {
|
||||
colonIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if colonIdx >= 0 {
|
||||
return trimSpace(trimmed[colonIdx+1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
197
internal/plugin/plugin_test.go
Normal file
197
internal/plugin/plugin_test.go
Normal file
@ -0,0 +1,197 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractDescription(t *testing.T) {
|
||||
content := "# Plugin\n\nThis is a test plugin.\nversion: 1.0.0"
|
||||
desc := extractDescription(content)
|
||||
if desc != "This is a test plugin." {
|
||||
t.Errorf("expected 'This is a test plugin.', got %q", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractField(t *testing.T) {
|
||||
content := "version: 1.0.0\nauthor: test\nio_type: io"
|
||||
if v := extractField(content, "version"); v != "1.0.0" {
|
||||
t.Errorf("expected '1.0.0', got %q", v)
|
||||
}
|
||||
if v := extractField(content, "author"); v != "test" {
|
||||
t.Errorf("expected 'test', got %q", v)
|
||||
}
|
||||
if v := extractField(content, "io_type"); v != "io" {
|
||||
t.Errorf("expected 'io', got %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFieldCaseInsensitive(t *testing.T) {
|
||||
content := "Version: 2.0.0"
|
||||
if v := extractField(content, "version"); v != "2.0.0" {
|
||||
t.Errorf("expected '2.0.0', got %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFieldMissing(t *testing.T) {
|
||||
if v := extractField("no fields here", "version"); v != "" {
|
||||
t.Errorf("expected '', got %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractIOConfigFull(t *testing.T) {
|
||||
content := `# QQ Plugin
|
||||
io_type: io
|
||||
io_input_route: qq
|
||||
io_output_route: qq
|
||||
io_output_caps: text,file,image`
|
||||
|
||||
cfg := extractIOConfig(content)
|
||||
if cfg == nil {
|
||||
t.Fatal("expected IOConfig")
|
||||
}
|
||||
if cfg.Type != "io" {
|
||||
t.Errorf("expected type 'io', got %q", cfg.Type)
|
||||
}
|
||||
if cfg.InputRoute != "qq" {
|
||||
t.Errorf("expected input_route 'qq', got %q", cfg.InputRoute)
|
||||
}
|
||||
if cfg.OutputRoute != "qq" {
|
||||
t.Errorf("expected output_route 'qq', got %q", cfg.OutputRoute)
|
||||
}
|
||||
if len(cfg.OutputCaps) != 3 || cfg.OutputCaps[0] != "text" {
|
||||
t.Errorf("expected caps [text file image], got %v", cfg.OutputCaps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractIOConfigMinimal(t *testing.T) {
|
||||
content := `# Plugin
|
||||
io_type: input`
|
||||
cfg := extractIOConfig(content)
|
||||
if cfg == nil {
|
||||
t.Fatal("expected IOConfig")
|
||||
}
|
||||
if cfg.Type != "input" {
|
||||
t.Errorf("expected 'input', got %q", cfg.Type)
|
||||
}
|
||||
if cfg.InputRoute != "" {
|
||||
t.Errorf("expected empty input_route, got %q", cfg.InputRoute)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractIOConfigNil(t *testing.T) {
|
||||
cfg := extractIOConfig("# No IO config here")
|
||||
if cfg != nil {
|
||||
t.Errorf("expected nil, got %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractToolDefsBasic(t *testing.T) {
|
||||
content := `# Plugin
|
||||
description
|
||||
|
||||
## hello_tool
|
||||
Say hello to someone
|
||||
- name: The person to greet
|
||||
|
||||
## add_numbers
|
||||
Add two numbers together
|
||||
- a: First number
|
||||
- b: Second number`
|
||||
|
||||
defs := extractToolDefs(content)
|
||||
if len(defs) != 2 {
|
||||
t.Fatalf("expected 2 tools, got %d", len(defs))
|
||||
}
|
||||
|
||||
if defs[0].Name != "hello_tool" {
|
||||
t.Errorf("expected 'hello_tool', got %q", defs[0].Name)
|
||||
}
|
||||
if defs[0].Description != "Say hello to someone" {
|
||||
t.Errorf("expected 'Say hello to someone', got %q", defs[0].Description)
|
||||
}
|
||||
|
||||
props := defs[0].Parameters["properties"].(map[string]interface{})
|
||||
if _, ok := props["name"]; !ok {
|
||||
t.Errorf("expected 'name' parameter")
|
||||
}
|
||||
p := props["name"].(map[string]interface{})
|
||||
if p["description"] != "The person to greet" {
|
||||
t.Errorf("expected desc 'The person to greet', got %q", p["description"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractToolDefsToolWithColon(t *testing.T) {
|
||||
content := `# Plugin
|
||||
|
||||
### Tool: my_tool
|
||||
Do something
|
||||
- param: Description`
|
||||
|
||||
defs := extractToolDefs(content)
|
||||
if len(defs) != 1 {
|
||||
t.Fatalf("expected 1 tool, got %d", len(defs))
|
||||
}
|
||||
if defs[0].Name != "my_tool" {
|
||||
t.Errorf("expected 'my_tool', got %q", defs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractToolDefsSkipsNonToolSections(t *testing.T) {
|
||||
content := `# Plugin
|
||||
|
||||
## Usage
|
||||
This is how to use the plugin
|
||||
|
||||
## Examples
|
||||
Some examples here
|
||||
|
||||
## real_tool
|
||||
This is an actual tool
|
||||
- param: value`
|
||||
|
||||
defs := extractToolDefs(content)
|
||||
if len(defs) != 1 {
|
||||
t.Fatalf("expected 1 tool (non-tool sections skipped), got %d", len(defs))
|
||||
}
|
||||
if defs[0].Name != "real_tool" {
|
||||
t.Errorf("expected 'real_tool', got %q", defs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractToolDefsEmpty(t *testing.T) {
|
||||
defs := extractToolDefs("# Just a title\nNo tools here")
|
||||
if len(defs) != 0 {
|
||||
t.Errorf("expected 0 tools, got %d", len(defs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractToolDefsCodeBlock(t *testing.T) {
|
||||
content := "# Plugin\n\n## my_tool\nA tool\n- param: desc\n\n```\n## not_a_tool\nThis is inside a code block\n```\n\n## another_tool\nAnother one\n- x: y"
|
||||
|
||||
defs := extractToolDefs(content)
|
||||
if len(defs) != 2 {
|
||||
t.Fatalf("expected 2 tools (code block skipped), got %d", len(defs))
|
||||
}
|
||||
if defs[0].Name != "my_tool" || defs[1].Name != "another_tool" {
|
||||
t.Errorf("unexpected tool names: %v", defs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractToolDefsNoParams(t *testing.T) {
|
||||
content := `# Plugin
|
||||
|
||||
## simple_tool
|
||||
A tool with no parameters`
|
||||
|
||||
defs := extractToolDefs(content)
|
||||
if len(defs) != 1 {
|
||||
t.Fatalf("expected 1 tool, got %d", len(defs))
|
||||
}
|
||||
if defs[0].Name != "simple_tool" {
|
||||
t.Errorf("expected 'simple_tool', got %q", defs[0].Name)
|
||||
}
|
||||
props := defs[0].Parameters["properties"].(map[string]interface{})
|
||||
if len(props) != 0 {
|
||||
t.Errorf("expected no params, got %d", len(props))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user