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
This commit is contained in:
JianFeeeee
2026-07-02 12:04:36 +08:00
parent 35a0932757
commit ce9a143d2f
43 changed files with 10296 additions and 0 deletions

899
internal/plugin/plugin.go Normal file
View File

@ -0,0 +1,899 @@
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
}