Files
HomeAgent/internal/plugin/plugin.go
root bc26850b50 feat: complete HomeAgent architecture v2
- IO abstraction layer with OutputChannel routing and capability validation
- Three-layer memory (Context-Document-Graph) with TF-IDF relevance pruning
- OneBot V11 QQ protocol plugin with Reverse WebSocket client
- Plugin system with hot-reload (SKILL.md + native factories)
- Knowledge system with TF-IDF vector indexing
- Personality system (personal.md)
- Text memory (JSONL with rotation)
- Change tracker (overlayfs) with rollback
- Lua adapter VM
- Design document (DESIGN.md)

Module: gitcode.com/JianFeeeee/HomeAgent
2026-07-02 12:04:36 +08:00

900 lines
23 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 plugin
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
type PluginType string
const (
PluginTypeSKILL PluginType = "skill"
PluginTypeNative PluginType = "native"
)
// IOConfig 定义插件作为 IO 通道时的配置
// 每个插件通过此配置声明自己的 I/O 端口
type IOConfig struct {
Type string `json:"type"` // "input" / "output" / "io"
InputRoute string `json:"input_route"` // 输入源标识,如 "qq", "email"
OutputRoute string `json:"output_route"` // 输出通道标识,默认等于 InputRoute
OutputCaps []string `json:"output_caps"` // 支持的输出能力: "text","file","image","audio","structured"
}
type Plugin interface {
Name() string
PluginType() PluginType
Description() string
Version() string
Tools() []ToolDef
Enabled() bool
SetEnabled(bool)
IOConfig() *IOConfig
Device() agentIO.Device // 内嵌的 IO 设备nil 表示纯技能插件
}
// ToolDef 复用 IO 抽象层的定义,确保 Plugin 和 Device 使用同一类型
type ToolDef = agentIO.ToolDef
type SKILLPlugin struct {
mu sync.RWMutex
name string
description string
version string
author string
enabled bool
rawContent string
sourceDir string
toolDefs []ToolDef
ioConfig *IOConfig
}
func LoadSKILL(path string) (*SKILLPlugin, error) {
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("stat %s: %w", path, err)
}
name := filepath.Base(path)
p := &SKILLPlugin{
name: name,
sourceDir: path,
enabled: true,
}
if info.IsDir() {
skillFile := filepath.Join(path, "SKILL.md")
if data, err := os.ReadFile(skillFile); err == nil {
p.rawContent = string(data)
p.description = extractDescription(p.rawContent)
p.version = extractField(p.rawContent, "version")
p.author = extractField(p.rawContent, "author")
p.ioConfig = extractIOConfig(p.rawContent)
}
metaFile := filepath.Join(path, "skill.json")
if data, err := os.ReadFile(metaFile); err == nil {
var meta struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Author string `json:"author"`
IO *IOConfig `json:"io,omitempty"`
}
if err := json.Unmarshal(data, &meta); err == nil {
if meta.Name != "" {
p.name = meta.Name
}
if meta.Description != "" {
p.description = meta.Description
}
if meta.Version != "" {
p.version = meta.Version
}
if meta.Author != "" {
p.author = meta.Author
}
if meta.IO != nil {
p.ioConfig = meta.IO
}
}
}
} else if filepath.Ext(path) == ".md" {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
p.rawContent = string(data)
p.name = name[:len(name)-3]
p.description = extractDescription(p.rawContent)
p.ioConfig = extractIOConfig(p.rawContent)
}
if p.rawContent != "" {
p.toolDefs = extractToolDefs(p.rawContent)
}
log.Printf("[plugin] loaded SKILL: %s v%s", p.name, p.version)
return p, nil
}
func (p *SKILLPlugin) Name() string { return p.name }
func (p *SKILLPlugin) PluginType() PluginType { return PluginTypeSKILL }
func (p *SKILLPlugin) Description() string { return p.description }
func (p *SKILLPlugin) Version() string { return p.version }
func (p *SKILLPlugin) Enabled() bool { p.mu.RLock(); defer p.mu.RUnlock(); return p.enabled }
func (p *SKILLPlugin) SetEnabled(v bool) { p.mu.Lock(); defer p.mu.Unlock(); p.enabled = v }
func (p *SKILLPlugin) Tools() []ToolDef { return p.toolDefs }
func (p *SKILLPlugin) IOConfig() *IOConfig { return p.ioConfig }
func (p *SKILLPlugin) Device() agentIO.Device { return nil } // SKILL 插件无原生 IO 设备
func (p *SKILLPlugin) RawContent() string { return p.rawContent }
// NativeFactory 是内置插件构造器,用于需要原生 Go 实现的插件(如 OneBot QQ
// 返回 agentIO.Device 以直接注册到 IO 管理层
type NativeFactory func(name string, config map[string]interface{}, iom *agentIO.IOManager) (agentIO.Device, error)
type Registry struct {
mu sync.RWMutex
plugins map[string]Plugin
ioMgr *agentIO.IOManager
factories map[string]NativeFactory // 名称匹配的插件使用原生实现
}
func NewRegistry() *Registry {
return &Registry{
plugins: make(map[string]Plugin),
factories: make(map[string]NativeFactory),
}
}
// RegisterNative 注册内置原生插件工厂。当从 plugins/ 加载插件时,
// 如果插件名称匹配已注册的工厂,优先使用原生设备注册。
// 例如: r.RegisterNative("qq", onebot.NewDeviceFactory)
func (r *Registry) RegisterNative(name string, factory NativeFactory) {
r.mu.Lock()
defer r.mu.Unlock()
r.factories[name] = factory
}
// SetIOManager 绑定 IO 管理器,启用 IO 设备自动注册
func (r *Registry) SetIOManager(mgr *agentIO.IOManager) {
r.mu.Lock()
defer r.mu.Unlock()
r.ioMgr = mgr
}
func (r *Registry) Register(p Plugin) {
r.mu.Lock()
defer r.mu.Unlock()
r.plugins[p.Name()] = p
log.Printf("[plugin] registered: %s (%s)", p.Name(), p.PluginType())
// 注册内嵌 IO 设备到 IOManager
if r.ioMgr != nil {
dev := p.Device()
if dev != nil {
// 原生设备直接注册
if err := r.ioMgr.RegisterDevice(dev); err != nil {
log.Printf("[plugin] register native device %s: %v", p.Name(), err)
return
}
} else if p.IOConfig() != nil {
// 有 IO 配置但无原生设备 → 用 PluginDevice 包装
dev = NewPluginDevice(p)
if err := r.ioMgr.RegisterDevice(dev); err != nil {
log.Printf("[plugin] register plugin device %s: %v", p.Name(), err)
return
}
} else {
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)
}
}
}
}
// nativePlugin 包装原生 IO 设备为 Plugin 接口
// Plugin 是容器Device 是组件
type nativePlugin struct {
name string
dev agentIO.Device
cfg *IOConfig
}
func (p *nativePlugin) Name() string { return p.name }
func (p *nativePlugin) PluginType() PluginType { return PluginTypeNative }
func (p *nativePlugin) Description() string { return p.dev.Description() }
func (p *nativePlugin) Version() string { return "1.0.0" }
func (p *nativePlugin) Tools() []ToolDef { return p.dev.Tools() }
func (p *nativePlugin) Enabled() bool { return true }
func (p *nativePlugin) SetEnabled(v bool) {}
func (p *nativePlugin) IOConfig() *IOConfig { return p.cfg }
func (p *nativePlugin) Device() agentIO.Device { return p.dev }
// Reload 原子化重载 plugins/ 目录:
// 1. 扫描磁盘加载新插件,构造并启动新 IO 设备
// 2. 原子替换 IOManager 的设备表与路由表
// 3. 停止并清理旧设备
// 全程无通信中断:旧设备持续服务直到路由切换完成
func (r *Registry) Reload(dir string) (string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return "插件目录不存在", nil
}
return "", fmt.Errorf("扫描插件目录: %w", err)
}
// 1. 扫描磁盘,并行加载新插件
type loadedPlugin struct {
name string
p Plugin
err error
}
var loaded []loadedPlugin
for _, entry := range entries {
name := entry.Name()
if !entry.IsDir() && !strings.HasSuffix(name, ".md") {
continue
}
if entry.IsDir() {
// 略过非插件目录(不含 SKILL.md
if _, err := os.Stat(filepath.Join(dir, name, "SKILL.md")); os.IsNotExist(err) {
continue
}
} else {
name = name[:len(name)-3]
}
r.mu.RLock()
_, exists := r.plugins[name]
r.mu.RUnlock()
if exists {
continue // 已存在,跳过(后续可用 diff 检测变更)
}
r.mu.RLock()
factory, hasFactory := r.factories[name]
r.mu.RUnlock()
if hasFactory {
dev, err := factory(name, r.configFor(name, dir), r.ioMgr)
if err != nil {
loaded = append(loaded, loadedPlugin{name: name, err: err})
continue
}
np := &nativePlugin{name: name, dev: dev}
np.cfg = extractIOConfig(r.readFile(filepath.Join(dir, name)))
loaded = append(loaded, loadedPlugin{name: name, p: np})
} else {
p, err := LoadSKILL(filepath.Join(dir, entry.Name()))
if err != nil {
loaded = append(loaded, loadedPlugin{name: name, err: err})
continue
}
loaded = append(loaded, loadedPlugin{name: name, p: p})
}
}
// 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)
continue
}
dev := lp.p.Device()
if dev == nil && lp.p.IOConfig() != nil {
dev = NewPluginDevice(lp.p)
}
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
}
}
}
}
// 3. 原子切换
r.mu.Lock()
var oldPlugins map[string]Plugin
if r.ioMgr != nil {
// 获取旧设备并原子替换
oldDevices := r.ioMgr.AtomicSwapDevices(newDevices, newRoutes)
// 停止旧设备
for _, dev := range oldDevices {
go dev.Stop()
}
} else {
for _, dev := range newDevices {
dev.Stop()
}
}
// 替换插件表
oldPlugins = r.plugins
r.plugins = make(map[string]Plugin)
for _, lp := range loaded {
if lp.p != nil {
r.plugins[lp.name] = lp.p
}
}
pluginCount := len(r.plugins)
ioCount := len(newDevices)
r.mu.Unlock()
// 4. 清理旧插件资源
for _, p := range oldPlugins {
_ = p
}
log.Printf("[plugin] atomic reload: %d plugins, %d io channels", pluginCount, ioCount)
return fmt.Sprintf("插件重载完成: %d 个插件, %d 个 IO 通道", pluginCount, ioCount), nil
}
func (r *Registry) Unregister(name string) {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.plugins[name]; ok && r.ioMgr != nil {
r.ioMgr.UnregisterDevice(name)
log.Printf("[plugin] unregistered io device: %s", name)
}
delete(r.plugins, name)
}
func (r *Registry) Get(name string) Plugin {
r.mu.RLock()
defer r.mu.RUnlock()
return r.plugins[name]
}
func (r *Registry) List() []Plugin {
r.mu.RLock()
defer r.mu.RUnlock()
list := make([]Plugin, 0, len(r.plugins))
for _, p := range r.plugins {
list = append(list, p)
}
sort.Slice(list, func(i, j int) bool {
return list[i].Name() < list[j].Name()
})
return list
}
func (r *Registry) ListEnabled() []Plugin {
r.mu.RLock()
defer r.mu.RUnlock()
var list []Plugin
for _, p := range r.plugins {
if p.Enabled() {
list = append(list, p)
}
}
return list
}
func (r *Registry) GetAllToolDefs() []ToolDef {
r.mu.RLock()
defer r.mu.RUnlock()
var defs []ToolDef
for _, p := range r.plugins {
if !p.Enabled() {
continue
}
defs = append(defs, p.Tools()...)
}
return defs
}
// HotReload 定期扫描插件目录,检测新增/变更/删除的插件并动态注册/注销
// interval=0 表示只执行一次扫描
func (r *Registry) HotReload(dir string, interval time.Duration, done <-chan struct{}) {
if interval <= 0 {
r.scanAndSync(dir)
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
r.scanAndSync(dir)
case <-done:
return
}
}
}
// scanAndSync 扫描插件目录并与当前注册表同步
func (r *Registry) scanAndSync(dir string) {
entries, err := os.ReadDir(dir)
if err != nil {
if !os.IsNotExist(err) {
log.Printf("[plugin] scan error: %v", err)
}
return
}
// 收集当前磁盘上的插件名
diskSet := make(map[string]bool)
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
diskSet[name] = true
} else if strings.HasSuffix(name, ".md") {
diskSet[name[:len(name)-3]] = true
}
}
r.mu.Lock()
// 移除已不存在的插件
for name := range r.plugins {
if !diskSet[name] {
if r.ioMgr != nil {
r.ioMgr.UnregisterDevice(name)
}
delete(r.plugins, name)
log.Printf("[plugin] hot-unload: %s", name)
}
}
r.mu.Unlock()
// 加载新增的插件
for _, entry := range entries {
path := filepath.Join(dir, entry.Name())
name := entry.Name()
if entry.IsDir() {
name = entry.Name()
} else if strings.HasSuffix(name, ".md") {
name = name[:len(name)-3]
} else {
continue
}
r.mu.RLock()
exists := r.plugins[name]
r.mu.RUnlock()
if exists != nil {
continue
}
// 检查是否有原生工厂
r.mu.RLock()
factory, hasFactory := r.factories[name]
r.mu.RUnlock()
if hasFactory {
// 使用原生设备,构造 Plugin 容器
dev, err := factory(name, r.configFor(name, dir), r.ioMgr)
if err != nil {
log.Printf("[plugin] native factory %s: %v", name, err)
continue
}
np := &nativePlugin{name: name, dev: dev}
np.cfg = extractIOConfig(r.readFile(path))
r.Register(np)
log.Printf("[plugin] hot-load native: %s", name)
} else {
var p Plugin
p, err = LoadSKILL(path)
if err != nil {
log.Printf("[plugin] hot-load skip %s: %v", entry.Name(), err)
continue
}
r.Register(p)
log.Printf("[plugin] hot-load: %s", p.Name())
}
}
}
// configFor 读取插件目录的 skill.json 作为原生工厂的配置
func (r *Registry) configFor(name, dir string) map[string]interface{} {
cfg := map[string]interface{}{}
path := filepath.Join(dir, name, "skill.json")
data, err := os.ReadFile(path)
if err != nil {
return cfg
}
var meta map[string]interface{}
if json.Unmarshal(data, &meta) == nil {
for k, v := range meta {
cfg[k] = v
}
}
return cfg
}
// readFile 读取插件目录的 SKILL.md
func (r *Registry) readFile(path string) string {
if info, err := os.Stat(path); err == nil && info.IsDir() {
data, _ := os.ReadFile(filepath.Join(path, "SKILL.md"))
return string(data)
}
data, _ := os.ReadFile(path)
return string(data)
}
func (r *Registry) LoadDir(dir string) error {
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
for _, entry := range entries {
path := filepath.Join(dir, entry.Name())
name := entry.Name()
if entry.IsDir() {
// 原生插件优先
r.mu.RLock()
factory, hasFactory := r.factories[name]
r.mu.RUnlock()
if hasFactory {
dev, err := factory(name, r.configFor(name, dir), r.ioMgr)
if err != nil {
log.Printf("[plugin] native factory %s: %v", name, err)
continue
}
r.mu.Lock()
r.plugins[name] = &nativePlugin{name: name, dev: dev}
r.mu.Unlock()
if r.ioMgr != nil {
r.ioMgr.RegisterDevice(dev)
log.Printf("[plugin] native: %s", name)
}
continue
}
p, err := LoadSKILL(path)
if err != nil {
log.Printf("[plugin] skip dir %s: %v", entry.Name(), err)
continue
}
r.Register(p)
} else if filepath.Ext(entry.Name()) == ".md" {
p, err := LoadSKILL(path)
if err != nil {
log.Printf("[plugin] skip file %s: %v", entry.Name(), err)
continue
}
r.Register(p)
}
}
return nil
}
// extractIOConfig parses IO port metadata from SKILL.md content
// Supported fields:
// io_type: input|output|io
// io_input_route: qq
// io_output_route: qq (optional, defaults to input_route)
// io_output_caps: text,file,image
func extractIOConfig(content string) *IOConfig {
ioType := extractField(content, "io_type")
if ioType == "" {
return nil
}
cfg := &IOConfig{
Type: ioType,
InputRoute: extractField(content, "io_input_route"),
}
if cfg.InputRoute == "" {
cfg.InputRoute = extractField(content, "io_route")
}
cfg.OutputRoute = extractField(content, "io_output_route")
if cfg.OutputRoute == "" {
cfg.OutputRoute = cfg.InputRoute
}
capsStr := extractField(content, "io_output_caps")
if capsStr != "" {
for _, c := range strings.Split(capsStr, ",") {
cfg.OutputCaps = append(cfg.OutputCaps, strings.TrimSpace(c))
}
}
return cfg
}
// PluginDevice 将 Plugin 包装为 IO Device实现热插拔
type PluginDevice struct {
plugin Plugin
caps agentIO.OutputCapability
}
func NewPluginDevice(p Plugin) *PluginDevice {
var caps agentIO.OutputCapability
if cfg := p.IOConfig(); cfg != nil {
// 有 IO 配置时使用声明的能力
for _, c := range cfg.OutputCaps {
switch strings.ToLower(c) {
case "text":
caps |= agentIO.CapText
case "file":
caps |= agentIO.CapFile
case "image":
caps |= agentIO.CapImage
case "audio":
caps |= agentIO.CapAudio
case "structured":
caps |= agentIO.CapStructured
}
}
}
// 无 IO 配置时 caps == 0 → 纯工具插件,不暴露为输出通道
return &PluginDevice{plugin: p, caps: caps}
}
func (d *PluginDevice) Name() string { return d.plugin.Name() }
func (d *PluginDevice) Type() agentIO.DeviceType {
if cfg := d.plugin.IOConfig(); cfg != nil {
switch cfg.Type {
case "input":
return agentIO.DeviceInput
case "output":
return agentIO.DeviceOutput
case "io":
return agentIO.DeviceIO
}
}
// 无 IO 配置 → 纯工具插件,归为 DeviceInput无输出能力
return agentIO.DeviceInput
}
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,
})
}
return defs
}
func (d *PluginDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
for _, t := range d.plugin.Tools() {
if t.Name == tool {
if t.Handler != nil {
return t.Handler(args)
}
return nil, fmt.Errorf("plugin %s: tool %s has no handler", d.plugin.Name(), tool)
}
}
return nil, fmt.Errorf("plugin %s: unknown tool %s", d.plugin.Name(), tool)
}
func (d *PluginDevice) Start() error { return nil }
func (d *PluginDevice) Stop() error { return nil }
func (d *PluginDevice) OutputCapabilities() agentIO.OutputCapability { return d.caps }
// extractDescription returns the first non-empty, non-header line
func extractDescription(content string) string {
for _, line := range splitLines(content) {
line = trimSpace(line)
if line != "" && !hasPrefix(line, "#") {
return line
}
}
return ""
}
// extractField finds `field: value` pattern in content
func extractField(content string, field string) string {
prefix := field + ":"
for _, line := range splitLines(content) {
trimmed := trimSpace(line)
if hasPrefix(toLower(trimmed), prefix) {
return trimSpace(trimPrefix(trimmed, prefix))
}
}
return ""
}
// extractToolDefs 从 SKILL.md 中解析工具定义OpenClaw 格式)
//
// 格式:
// ## tool_name
// 工具描述
// - param1: 参数描述
// - param2: 参数描述
//
// 也支持:
// ### Tool: tool_name
// 格式(三级标题)
func extractToolDefs(content string) []ToolDef {
lines := splitLines(content)
var defs []ToolDef
var currentTool *ToolDef
inCodeBlock := false
for i := 0; i < len(lines); i++ {
line := lines[i]
trimmed := trimSpace(line)
// 跳过代码块
if strings.HasPrefix(trimmed, "```") {
inCodeBlock = !inCodeBlock
continue
}
if inCodeBlock {
continue
}
// 检测工具定义开始: ## tool_name 或 ### Tool: tool_name
if strings.HasPrefix(trimmed, "## ") && !strings.HasPrefix(trimmed, "### ") {
// 结束上一个工具
if currentTool != nil && currentTool.Name != "" {
defs = append(defs, *currentTool)
}
currentTool = &ToolDef{}
namePart := strings.TrimPrefix(trimmed, "## ")
// 跳过已知的非工具章节
if isNonToolSection(namePart) {
currentTool = nil
continue
}
currentTool.Name = namePart
currentTool.Parameters = map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
}
continue
}
// 也支持 ### Tool: name 格式
if strings.HasPrefix(trimmed, "### Tool: ") {
if currentTool != nil && currentTool.Name != "" {
defs = append(defs, *currentTool)
}
currentTool = &ToolDef{}
namePart := strings.TrimPrefix(trimmed, "### Tool: ")
currentTool.Name = namePart
currentTool.Parameters = map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
}
continue
}
if currentTool == nil {
continue
}
// 如果 name 为空且已进入工具段,跳过
if currentTool.Name == "" {
continue
}
// 描述行:第一个非空、非标题、非列表行
if currentTool.Description == "" && trimmed != "" &&
!strings.HasPrefix(trimmed, "- ") &&
!strings.HasPrefix(trimmed, "#") {
currentTool.Description = trimmed
continue
}
// 参数行:- param: description
if strings.HasPrefix(trimmed, "- ") {
paramLine := strings.TrimPrefix(trimmed, "- ")
colonIdx := strings.Index(paramLine, ":")
if colonIdx > 0 {
paramName := strings.TrimSpace(paramLine[:colonIdx])
paramDesc := strings.TrimSpace(paramLine[colonIdx+1:])
if paramName != "" {
props := currentTool.Parameters["properties"].(map[string]interface{})
props[paramName] = map[string]interface{}{
"type": "string",
"description": paramDesc,
}
}
}
}
}
// 收尾最后一个工具
if currentTool != nil && currentTool.Name != "" {
defs = append(defs, *currentTool)
}
return defs
}
// isNonToolSection 判断是否为非工具章节(如 Usage、Examples、Installation 等)
func isNonToolSection(name string) bool {
lower := strings.ToLower(name)
skip := []string{
"tools", "usage", "examples", "installation", "setup",
"configuration", "overview", "description", "notes",
"parameters", "return", "returns", "options", "syntax",
}
for _, s := range skip {
if lower == s || strings.HasPrefix(lower, s+" ") || strings.HasPrefix(lower, s+":") {
return true
}
}
return false
}
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i <= len(s); i++ {
if i == len(s) || s[i] == '\n' {
if i > start {
lines = append(lines, s[start:i])
}
start = i + 1
}
}
return lines
}
func trimSpace(s string) string {
start, end := 0, len(s)
for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\r') {
start++
}
for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\r') {
end--
}
return s[start:end]
}
func hasPrefix(s, prefix string) bool {
if len(s) < len(prefix) {
return false
}
return s[:len(prefix)] == prefix
}
func toLower(s string) string {
b := make([]byte, len(s))
for i := 0; i < len(s); i++ {
if s[i] >= 'A' && s[i] <= 'Z' {
b[i] = s[i] + 32
} else {
b[i] = s[i]
}
}
return string(b)
}
func trimPrefix(s, prefix string) string {
if hasPrefix(s, prefix) {
return s[len(prefix):]
}
return s
}