fd44b15d81
**死锁根因修复** - periodicThinkLoop:1015 orphaned lock → 删除(字段已原子化) - RecordUserMessage 隔离为 recordMu - atomic.Int64 替换 lastUserMessage/lastThinkTime 等 **MD3 / Android 17 主题** - 毛玻璃卡片 (backdrop-filter) - MD3 色彩令牌 (pink primary #f472b6) - icons.js 独立矢量图标库 + 运行时 emoji 替换 - 无边框卡片、圆角按钮、阴影层次 **上下文持久化** - AddMessage → saveToDB 异步写 PostgreSQL - LoadFromDB 恢复 (admin-session-main + 懒加载) - LLMMessage.Timestamp 字段 **群聊与适配器** - group_ambient 模式: 非@消息让 LLM 自己判断是否插话 - 戳一戳动作消息总是回复 - NapCat 打字状态 (set_input_status, 最小3秒显示) - HTTP API 配置 (http_url/http_token) **知识库 & 防编造** - knowledge.CanHandle 对 chat 意图也触发 - 关键词预筛选避免无关 embedding 调用 - persona + synthesizer 三重诚实规则 - 工具结果持久化到会话历史 **平台桥接器** - detached:true Go进程独立存活 - ethend 重启自动接管已运行服务 - stop() 接管模式 taskkill/F/ PID - Windows netstat 替代 fuser 获取 PID - 重复适配器种子逻辑修复 - 失败转发日志 Direction: error **崩溃诊断** - crashlog 包 (Recover + WrapHTTP + LLMCall) - /api/v1/debug/goroutines 端点 - thinker 操作日志 + 30s stats - 日志写入 logs/ 目录持久化 Co-Authored-By: Claude <noreply@anthropic.com>
250 lines
6.7 KiB
Go
250 lines
6.7 KiB
Go
//go:build ignore
|
|
|
|
// gen_plugins generates plugins_gen.go from:
|
|
// 1. plugins.json — built-in plugins (in cyrene-plugins)
|
|
// 2. ../plugins/*/plugin.json — user plugins (auto-discovered)
|
|
//
|
|
// Usage: go run gen_plugins.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type PluginEntry struct {
|
|
Name string `json:"name"`
|
|
Import string `json:"import,omitempty"` // built-in: explicit import
|
|
Struct string `json:"struct"`
|
|
Constructor string `json:"constructor,omitempty"`
|
|
}
|
|
|
|
type PluginManifest struct {
|
|
Name string `json:"name"`
|
|
Struct string `json:"struct"`
|
|
}
|
|
|
|
// ── load built-in plugins from plugins.json ──
|
|
|
|
func loadBuiltinPlugins() ([]PluginEntry, error) {
|
|
data, err := os.ReadFile("../plugins.json")
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("read plugins.json: %w", err)
|
|
}
|
|
var cfg struct {
|
|
Version string `json:"version"`
|
|
Plugins []PluginEntry `json:"plugins"`
|
|
}
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parse plugins.json: %w", err)
|
|
}
|
|
return cfg.Plugins, nil
|
|
}
|
|
|
|
// ── auto-discover user plugins from ../plugins/ ──
|
|
|
|
func discoverUserPlugins() ([]PluginEntry, error) {
|
|
pluginsDir := filepath.Join("..", "..", "plugins")
|
|
entries, err := os.ReadDir(pluginsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("read plugins dir: %w", err)
|
|
}
|
|
|
|
var result []PluginEntry
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
dir := filepath.Join(pluginsDir, e.Name())
|
|
|
|
// 必须有 plugin.json
|
|
manifestPath := filepath.Join(dir, "plugin.json")
|
|
manifestData, err := os.ReadFile(manifestPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var m PluginManifest
|
|
if err := json.Unmarshal(manifestData, &m); err != nil {
|
|
fmt.Fprintf(os.Stderr, "⚠ skip %s: bad plugin.json: %v\n", e.Name(), err)
|
|
continue
|
|
}
|
|
if m.Name == "" {
|
|
m.Name = e.Name()
|
|
}
|
|
|
|
// 从 go.mod 读取模块路径作为 import
|
|
modPath := filepath.Join(dir, "go.mod")
|
|
modData, err := os.ReadFile(modPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "⚠ skip %s: no go.mod: %v\n", e.Name(), err)
|
|
continue
|
|
}
|
|
importPath := parseModule(modData)
|
|
if importPath == "" {
|
|
fmt.Fprintf(os.Stderr, "⚠ skip %s: cannot parse module from go.mod\n", e.Name())
|
|
continue
|
|
}
|
|
|
|
result = append(result, PluginEntry{
|
|
Name: m.Name,
|
|
Import: importPath,
|
|
Struct: m.Struct,
|
|
})
|
|
fmt.Printf(" ✓ discovered %s → %s\n", m.Name, importPath)
|
|
}
|
|
|
|
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
|
|
return result, nil
|
|
}
|
|
|
|
func parseModule(data []byte) string {
|
|
lines := strings.Split(string(data), "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "module ") {
|
|
return strings.TrimSpace(strings.TrimPrefix(line, "module "))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ── generate plugins_gen.go ──
|
|
|
|
func generate(plugins []PluginEntry) error {
|
|
var sb strings.Builder
|
|
sb.WriteString("// Code generated by gen_plugins.go; DO NOT EDIT.\n\n")
|
|
sb.WriteString("package main\n\n")
|
|
sb.WriteString("import (\n")
|
|
sb.WriteString("\tplgSDK \"git.yeij.top/AskaEth/Cyrene-Plugins/sdk\"\n")
|
|
|
|
aliases := make(map[string]string)
|
|
imported := make(map[string]bool)
|
|
for _, p := range plugins {
|
|
pkgAlias := p.Name + "pkg"
|
|
aliases[p.Name] = pkgAlias
|
|
if !imported[p.Import] {
|
|
sb.WriteString(fmt.Sprintf("\t%s \"%s\"\n", pkgAlias, p.Import))
|
|
imported[p.Import] = true
|
|
}
|
|
}
|
|
|
|
sb.WriteString(")\n\n")
|
|
sb.WriteString("func registerPlugins(registry interface{ Register(plgSDK.Tool) error }) {\n")
|
|
|
|
for _, p := range plugins {
|
|
alias := aliases[p.Name]
|
|
if p.Constructor != "" {
|
|
if p.Name == "file_ops" || p.Name == "http_request" {
|
|
sb.WriteString(fmt.Sprintf("\tfor _, t := range %s.%s(nil).Tools() {\n", alias, p.Constructor))
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf("\tfor _, t := range %s.%s().Tools() {\n", alias, p.Constructor))
|
|
}
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf("\tfor _, t := range (&%s.%s{}).Tools() {\n", alias, p.Struct))
|
|
}
|
|
sb.WriteString("\t\tregistry.Register(t)\n")
|
|
sb.WriteString("\t}\n")
|
|
}
|
|
|
|
sb.WriteString("}\n")
|
|
|
|
outPath := "plugins_gen.go"
|
|
if err := os.WriteFile(outPath, []byte(sb.String()), 0644); err != nil {
|
|
return fmt.Errorf("write %s: %w", outPath, err)
|
|
}
|
|
fmt.Printf("Generated %s with %d plugins\n", outPath, len(plugins))
|
|
return nil
|
|
}
|
|
|
|
// ── auto-add require + replace directives to go.mod ──
|
|
|
|
func ensureGoMod(userPlugins []PluginEntry) error {
|
|
modPath := filepath.Join("..", "go.mod")
|
|
data, err := os.ReadFile(modPath)
|
|
if err != nil {
|
|
return fmt.Errorf("read go.mod: %w", err)
|
|
}
|
|
content := string(data)
|
|
pluginsDir := filepath.Join("..", "..", "plugins")
|
|
entries, _ := os.ReadDir(pluginsDir)
|
|
|
|
for _, e := range entries {
|
|
if !e.IsDir() { continue }
|
|
dir := filepath.Join(pluginsDir, e.Name())
|
|
modData, err := os.ReadFile(filepath.Join(dir, "go.mod"))
|
|
if err != nil { continue }
|
|
mod := parseModule(modData)
|
|
if mod == "" { continue }
|
|
|
|
relPath, _ := filepath.Rel(filepath.Join(".."), dir)
|
|
relPath = strings.ReplaceAll(relPath, "\\", "/")
|
|
|
|
// ensure require
|
|
reqLine := fmt.Sprintf("\t%s v0.0.0\n", mod)
|
|
if !strings.Contains(content, reqLine) {
|
|
reqBlock := strings.Index(content, "require (")
|
|
if reqBlock < 0 { continue }
|
|
closeIdx := strings.Index(content[reqBlock:], ")")
|
|
if closeIdx < 0 { continue }
|
|
insertAt := reqBlock + closeIdx
|
|
content = content[:insertAt] + reqLine + content[insertAt:]
|
|
}
|
|
|
|
// ensure replace
|
|
replaceLine := fmt.Sprintf("\t%s => %s\n", mod, relPath)
|
|
if !strings.Contains(content, replaceLine) {
|
|
replaceBlock := strings.Index(content, "replace (")
|
|
if replaceBlock < 0 {
|
|
content += fmt.Sprintf("\nreplace (\n%s)\n", replaceLine)
|
|
} else {
|
|
closeIdx := strings.Index(content[replaceBlock:], ")")
|
|
if closeIdx < 0 { continue }
|
|
insertAt := replaceBlock + closeIdx
|
|
content = content[:insertAt] + replaceLine + content[insertAt:]
|
|
}
|
|
}
|
|
}
|
|
|
|
return os.WriteFile(modPath, []byte(content), 0644)
|
|
}
|
|
|
|
func main() {
|
|
builtins, err := loadBuiltinPlugins()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "load builtins: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Built-in plugins: %d\n", len(builtins))
|
|
|
|
fmt.Println("Discovering user plugins...")
|
|
userPlugins, err := discoverUserPlugins()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "discover user plugins: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// 自动补 go.mod replace 指令
|
|
if len(userPlugins) > 0 {
|
|
if err := ensureGoMod(userPlugins); err != nil {
|
|
fmt.Fprintf(os.Stderr, "ensure go.mod: %v\n", err)
|
|
}
|
|
}
|
|
|
|
all := append(builtins, userPlugins...)
|
|
if err := generate(all); err != nil {
|
|
fmt.Fprintf(os.Stderr, "generate: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|