Files
tao-memory-mcp/tao_core.go
2026-03-15 04:47:03 +08:00

728 lines
19 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 main
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
// --- 道 (Config & State) ---
type Config struct {
MemoryRoot string
Port string
SearchRoot string
MaxSearchFiles int
}
type TaoServer struct {
config Config
mu sync.Mutex // 确保并发写入安全
conns sync.Map // token -> chan string
}
// --- 一、二、三 (Time Logic) ---
// GetTaoPath 根据道家哲学计算时间路径
func (s *TaoServer) GetTaoPath() (string, string) {
return s.GetTaoPathFromTime(time.Now())
}
// GetTaoPathFromTime 根据指定时间计算路径
func (s *TaoServer) GetTaoPathFromTime(t time.Time) (string, string) {
year := t.Format("2006")
month := int(t.Month())
_, isoWeek := t.ISOWeek()
// 一生二:上半年(H1_Upper) / 下半年(H2_Lower)
half := "H1_Upper"
if month > 6 {
half = "H2_Lower"
}
// 二生三:季度
quarter := fmt.Sprintf("Q%d", (month-1)/3+1)
// 三生万物:月与周
monthDir := fmt.Sprintf("%02d_%s", month, t.Month().String())
weekDir := fmt.Sprintf("W%02d", isoWeek)
dirPath := filepath.Join(s.config.MemoryRoot, year, half, quarter, monthDir, weekDir)
fileName := t.Format("2006-01-02_Monday.md")
return dirPath, fileName
}
func (s *TaoServer) getMonthDirFromTime(t time.Time) string {
year := t.Format("2006")
month := int(t.Month())
half := "H1_Upper"
if month > 6 {
half = "H2_Lower"
}
quarter := fmt.Sprintf("Q%d", (month-1)/3+1)
monthDir := fmt.Sprintf("%02d_%s", month, t.Month().String())
return filepath.Join(s.config.MemoryRoot, year, half, quarter, monthDir)
}
func (s *TaoServer) getQuarterDirFromTime(t time.Time) string {
year := t.Format("2006")
month := int(t.Month())
half := "H1_Upper"
if month > 6 {
half = "H2_Lower"
}
quarter := fmt.Sprintf("Q%d", (month-1)/3+1)
return filepath.Join(s.config.MemoryRoot, year, half, quarter)
}
func (s *TaoServer) getHalfDirFromTime(t time.Time) string {
year := t.Format("2006")
month := int(t.Month())
half := "H1_Upper"
if month > 6 {
half = "H2_Lower"
}
return filepath.Join(s.config.MemoryRoot, year, half)
}
func (s *TaoServer) getYearDirFromTime(t time.Time) string {
return filepath.Join(s.config.MemoryRoot, t.Format("2006"))
}
func (s *TaoServer) ApplyWeeklyTemplateIfMissing(content string, weekOffset int) string {
if strings.Contains(content, "### 📅 时空坐标") || strings.Contains(content, "## 📅 时空坐标") {
return content
}
targetTime := time.Now().AddDate(0, 0, weekOffset*7)
year, week := targetTime.ISOWeek()
period := fmt.Sprintf("%d年第%d周", year, week)
refinedAt := time.Now().Format("2006-01-02")
template := fmt.Sprintf(`### 📅 时空坐标
* **周期**%s
* **炼化时间**%s
### ⚡ 核心突破 (Major Breakthroughs)
%s
### 🏮 避坑/因果记录 (Pitfalls & Karma)
-
### 🧪 炼化所得 (Extracted Essence)
-
### 🚀 下一轮循环 (Next Cycle)
- [ ]
### 🧧 炼丹师评注
-
`, period, refinedAt, strings.TrimSpace(content))
return template
}
func (s *TaoServer) CaptureIdea(content string, tags []string) (string, error) {
if len(tags) == 0 {
tags = []string{"Unsorted"}
}
now := time.Now()
fileName := fmt.Sprintf("Idea_%d.md", now.Unix())
dirPath := filepath.Join(s.config.MemoryRoot, "Inspirations")
if err := os.MkdirAll(dirPath, 0755); err != nil {
return "", err
}
var buf strings.Builder
buf.WriteString("---\n")
buf.WriteString(fmt.Sprintf("date: %s\n", now.Format("2006-01-02 15:04:05")))
buf.WriteString("type: inspiration\n")
if len(tags) > 0 {
buf.WriteString("tags:\n")
for _, tag := range tags {
buf.WriteString(fmt.Sprintf(" - %s\n", tag))
}
}
buf.WriteString("---\n\n")
buf.WriteString(content)
filePath := filepath.Join(dirPath, fileName)
if err := os.WriteFile(filePath, []byte(buf.String()), 0644); err != nil {
return "", err
}
summary := content
if len([]rune(summary)) > 40 {
summary = string([]rune(summary)[:40]) + "..."
}
_ = s.Record("idea", fmt.Sprintf("💡 %s (归档: %s)", summary, fileName), 2)
return fmt.Sprintf("灵感已归档: %s", filePath), nil
}
// GetMonthData 读取指定月份目录下所有 Week_Summary.md
func (s *TaoServer) GetMonthData(monthOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, monthOffset, 0)
monthDir := s.getMonthDirFromTime(targetTime)
var monthContent strings.Builder
monthContent.WriteString(fmt.Sprintf("# 待炼化月素材: %s\n\n", monthDir))
err := filepath.Walk(monthDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() == "Week_Summary.md" {
content, err := os.ReadFile(path)
if err != nil {
return nil
}
rel, _ := filepath.Rel(monthDir, path)
monthContent.WriteString(fmt.Sprintf("## 来源: %s\n%s\n\n", rel, string(content)))
}
return nil
})
return monthContent.String(), err
}
// RecordMonthSummary 将炼化后的内容写入 Month_Summary.md
func (s *TaoServer) RecordMonthSummary(content string, monthOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, monthOffset, 0)
monthDir := s.getMonthDirFromTime(targetTime)
if err := os.MkdirAll(monthDir, 0755); err != nil {
return "", err
}
summaryPath := filepath.Join(monthDir, "Month_Summary.md")
header := fmt.Sprintf("---\ntype: summary\nlevel: month\nrefined_at: %s\n---\n\n", time.Now().Format(time.RFC3339))
err := os.WriteFile(summaryPath, []byte(header+content), 0644)
if err != nil {
return "", err
}
return summaryPath, nil
}
// GetQuarterData 读取指定季度目录下所有 Month_Summary.md
func (s *TaoServer) GetQuarterData(quarterOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, quarterOffset*3, 0)
quarterDir := s.getQuarterDirFromTime(targetTime)
var quarterContent strings.Builder
quarterContent.WriteString(fmt.Sprintf("# 待炼化季素材: %s\n\n", quarterDir))
err := filepath.Walk(quarterDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() == "Month_Summary.md" {
content, err := os.ReadFile(path)
if err != nil {
return nil
}
rel, _ := filepath.Rel(quarterDir, path)
quarterContent.WriteString(fmt.Sprintf("## 来源: %s\n%s\n\n", rel, string(content)))
}
return nil
})
return quarterContent.String(), err
}
// RecordQuarterSummary 将炼化后的内容写入 Quarter_Summary.md
func (s *TaoServer) RecordQuarterSummary(content string, quarterOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, quarterOffset*3, 0)
quarterDir := s.getQuarterDirFromTime(targetTime)
if err := os.MkdirAll(quarterDir, 0755); err != nil {
return "", err
}
summaryPath := filepath.Join(quarterDir, "Quarter_Summary.md")
header := fmt.Sprintf("---\ntype: summary\nlevel: quarter\nrefined_at: %s\n---\n\n", time.Now().Format(time.RFC3339))
err := os.WriteFile(summaryPath, []byte(header+content), 0644)
if err != nil {
return "", err
}
return summaryPath, nil
}
// GetSemiannualData 读取指定半年目录下所有 Quarter_Summary.md
func (s *TaoServer) GetSemiannualData(halfOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, halfOffset*6, 0)
halfDir := s.getHalfDirFromTime(targetTime)
var halfContent strings.Builder
halfContent.WriteString(fmt.Sprintf("# 待炼化半年素材: %s\n\n", halfDir))
err := filepath.Walk(halfDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() == "Quarter_Summary.md" {
content, err := os.ReadFile(path)
if err != nil {
return nil
}
rel, _ := filepath.Rel(halfDir, path)
halfContent.WriteString(fmt.Sprintf("## 来源: %s\n%s\n\n", rel, string(content)))
}
return nil
})
return halfContent.String(), err
}
// RecordSemiannualSummary 将炼化后的内容写入 Semiannual_Summary.md
func (s *TaoServer) RecordSemiannualSummary(content string, halfOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, halfOffset*6, 0)
halfDir := s.getHalfDirFromTime(targetTime)
if err := os.MkdirAll(halfDir, 0755); err != nil {
return "", err
}
summaryPath := filepath.Join(halfDir, "Semiannual_Summary.md")
header := fmt.Sprintf("---\ntype: summary\nlevel: semiannual\nrefined_at: %s\n---\n\n", time.Now().Format(time.RFC3339))
err := os.WriteFile(summaryPath, []byte(header+content), 0644)
if err != nil {
return "", err
}
return summaryPath, nil
}
// GetYearData 读取指定年度目录下所有 Semiannual_Summary.md
func (s *TaoServer) GetYearData(yearOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(yearOffset, 0, 0)
yearDir := s.getYearDirFromTime(targetTime)
var yearContent strings.Builder
yearContent.WriteString(fmt.Sprintf("# 待炼化年素材: %s\n\n", yearDir))
err := filepath.Walk(yearDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() == "Semiannual_Summary.md" {
content, err := os.ReadFile(path)
if err != nil {
return nil
}
rel, _ := filepath.Rel(yearDir, path)
yearContent.WriteString(fmt.Sprintf("## 来源: %s\n%s\n\n", rel, string(content)))
}
return nil
})
return yearContent.String(), err
}
// RecordYearSummary 将炼化后的内容写入 Year_Summary.md
func (s *TaoServer) RecordYearSummary(content string, yearOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(yearOffset, 0, 0)
yearDir := s.getYearDirFromTime(targetTime)
if err := os.MkdirAll(yearDir, 0755); err != nil {
return "", err
}
summaryPath := filepath.Join(yearDir, "Year_Summary.md")
header := fmt.Sprintf("---\ntype: summary\nlevel: year\nrefined_at: %s\n---\n\n", time.Now().Format(time.RFC3339))
err := os.WriteFile(summaryPath, []byte(header+content), 0644)
if err != nil {
return "", err
}
return summaryPath, nil
}
func (s *TaoServer) HousekeepMemory(targetMonth string) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
parts := strings.Split(targetMonth, "-")
if len(parts) != 2 {
return "", fmt.Errorf("invalid target_month format, expected YYYY-MM")
}
year, month := parts[0], parts[1]
if len(year) != 4 || len(month) != 2 {
return "", fmt.Errorf("invalid target_month format, expected YYYY-MM")
}
monthNum, err := strconv.Atoi(month)
if err != nil || monthNum < 1 || monthNum > 12 {
return "", fmt.Errorf("invalid month number")
}
t := time.Date(mustAtoi(year), time.Month(monthNum), 1, 0, 0, 0, 0, time.Local)
monthDir := s.getMonthDirFromTime(t)
summaryPath := filepath.Join(monthDir, "Month_Summary.md")
info, err := os.Stat(summaryPath)
if err != nil {
return "", fmt.Errorf("Month_Summary.md not found: %s", summaryPath)
}
if info.Size() < 100 {
return "", fmt.Errorf("Month_Summary.md too small, skip archive")
}
content, err := os.ReadFile(summaryPath)
if err != nil {
return "", err
}
if !strings.Contains(string(content), "###") {
return "", fmt.Errorf("Month_Summary.md missing expected headings")
}
archiveRoot := filepath.Join(s.config.MemoryRoot, "_Archive", year, month)
if err := os.MkdirAll(archiveRoot, 0755); err != nil {
return "", err
}
entries, err := os.ReadDir(monthDir)
if err != nil {
return "", err
}
for _, entry := range entries {
if entry.IsDir() && strings.HasPrefix(entry.Name(), "W") {
src := filepath.Join(monthDir, entry.Name())
dst := filepath.Join(archiveRoot, entry.Name())
if err := movePath(src, dst); err != nil {
return "", err
}
}
if !entry.IsDir() && entry.Name() != "Month_Summary.md" {
src := filepath.Join(monthDir, entry.Name())
dst := filepath.Join(archiveRoot, entry.Name())
if err := movePath(src, dst); err != nil {
return "", err
}
}
}
marker := fmt.Sprintf("原始数据已归档至 %s 于 %s\n", archiveRoot, time.Now().Format("2006-01-02 15:04:05"))
_ = os.WriteFile(filepath.Join(monthDir, "ARCHIVED.txt"), []byte(marker), 0644)
return fmt.Sprintf("归档完成: %s", archiveRoot), nil
}
func movePath(src string, dst string) error {
if err := os.Rename(src, dst); err == nil {
return nil
}
info, err := os.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return copyDir(src, dst)
}
if err := copyFile(src, dst); err != nil {
return err
}
return os.RemoveAll(src)
}
func copyDir(src string, dst string) error {
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
entries, err := os.ReadDir(src)
if err != nil {
return err
}
for _, entry := range entries {
sPath := filepath.Join(src, entry.Name())
dPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
if err := copyDir(sPath, dPath); err != nil {
return err
}
} else {
if err := copyFile(sPath, dPath); err != nil {
return err
}
}
}
return nil
}
func copyFile(src string, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
if err := out.Sync(); err != nil {
return err
}
return nil
}
func mustAtoi(s string) int {
n, _ := strconv.Atoi(s)
return n
}
func (s *TaoServer) InspectAndPropose(repoPath string) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if repoPath == "" {
repoPath = "/root/.openclaw/workspace/tao_mcp_go"
}
allowed := getEnv("TAO_ALLOWED_REPOS", repoPath)
allowList := strings.Split(allowed, ",")
permitted := false
for _, item := range allowList {
item = strings.TrimSpace(item)
if item != "" && repoPath == item {
permitted = true
break
}
}
if !permitted {
return "repo_path not allowed", fmt.Errorf("repo_path not allowed: %s", repoPath)
}
if getEnvBool("TAO_ALLOW_GIT_PULL", false) {
_ = exec.Command("git", "-C", repoPath, "pull").Run()
}
// 2) 收集灵感(包含 #Todo/#Fix
inspDir := filepath.Join(s.config.MemoryRoot, "Inspirations")
var ideas []string
_ = filepath.Walk(inspDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || filepath.Ext(path) != ".md" {
return nil
}
b, err := os.ReadFile(path)
if err != nil {
return nil
}
text := string(b)
if strings.Contains(text, "#Todo") || strings.Contains(text, "#Fix") {
ideas = append(ideas, text)
}
return nil
})
if len(ideas) == 0 {
return "未发现 #Todo/#Fix 灵感记录,跳过。", nil
}
// 3) 写入待办清单文件(供 Agent 生成补丁时参考)
patchDir := filepath.Join(s.config.MemoryRoot, "_Proposals")
_ = os.MkdirAll(patchDir, 0755)
proposalPath := filepath.Join(patchDir, fmt.Sprintf("proposal_%s.md", time.Now().Format("20060102_150405")))
body := "# 灵感-代码对照清单\n\n" + strings.Join(ideas, "\n\n---\n\n")
_ = os.WriteFile(proposalPath, []byte(body), 0644)
// 4) 记录到当日日志
summary := fmt.Sprintf("生成灵感对照清单:%s共 %d 条)", proposalPath, len(ideas))
_ = s.Record("inspect", summary, 3)
return summary, nil
}
func (s *TaoServer) RecordDaily(content string, karma int) (string, error) {
if karma == 0 {
karma = 1
}
if err := s.Record("daily", content, karma); err != nil {
return "", err
}
dir, file := s.GetTaoPath()
return filepath.Join(dir, file), nil
}
// --- 以简御繁 (Core Logic) ---
// Record 将提炼后的精华存入时间流
func (s *TaoServer) Record(category, content string, karma int) error {
s.mu.Lock()
defer s.mu.Unlock()
dir, file := s.GetTaoPath()
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
fullPath := filepath.Join(dir, file)
f, err := os.OpenFile(fullPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
entry := fmt.Sprintf("### [%s] [%s] (Karma:%d)\n%s\n\n",
time.Now().Format("15:04:05"), category, karma, content)
_, err = f.WriteString(entry)
return err
}
// GetWeekData 读取指定周目录下所有的每日记录,为“炼化”准备素材
func (s *TaoServer) GetWeekData(weekOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, 0, weekOffset*7)
targetDir, _ := s.GetTaoPathFromTime(targetTime)
files, err := os.ReadDir(targetDir)
if err != nil {
return "", err
}
var weekContent strings.Builder
weekContent.WriteString(fmt.Sprintf("# 待炼化周素材: %s\n\n", targetDir))
for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".md" && file.Name() != "Week_Summary.md" {
content, _ := os.ReadFile(filepath.Join(targetDir, file.Name()))
weekContent.WriteString(fmt.Sprintf("## 来源: %s\n%s\n\n", file.Name(), string(content)))
}
}
return weekContent.String(), nil
}
// RecordSummary 将炼化后的内容写入 Week_Summary.md
func (s *TaoServer) RecordSummary(content string, weekOffset int) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
targetTime := time.Now().AddDate(0, 0, weekOffset*7)
targetDir, _ := s.GetTaoPathFromTime(targetTime)
// 周总结在周目录下
summaryPath := filepath.Join(targetDir, "Week_Summary.md")
header := fmt.Sprintf("---\ntype: summary\nrefined_at: %s\n---\n\n", time.Now().Format(time.RFC3339))
err := os.WriteFile(summaryPath, []byte(header+content), 0644)
if err != nil {
return "", err
}
return summaryPath, nil
}
// SearchMemoryAdvanced 遍历所有 Markdown 文件,寻找包含关键词的内容
func (s *TaoServer) SearchMemoryAdvanced(keyword string, related []string, causal bool, includeArchive bool) ([]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
type hit struct {
term string
causal bool
content string
}
hits := []hit{}
terms := []string{keyword}
if causal {
for _, t := range related {
if t != "" && t != keyword {
terms = append(terms, t)
}
}
}
scanned := 0
err := filepath.Walk(s.config.SearchRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if s.config.MaxSearchFiles > 0 && scanned >= s.config.MaxSearchFiles {
return filepath.SkipDir
}
if !includeArchive && info.IsDir() && strings.Contains(path, string(filepath.Separator)+"_Archive"+string(filepath.Separator)) {
return filepath.SkipDir
}
if !info.IsDir() && filepath.Ext(path) == ".md" {
scanned++
if !includeArchive && strings.Contains(path, string(filepath.Separator)+"_Archive"+string(filepath.Separator)) {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return nil
}
text := string(content)
for _, term := range terms {
if term != "" && strings.Contains(text, term) {
rel, _ := filepath.Rel(s.config.SearchRoot, path)
label := "命中"
isCausal := term != keyword
if isCausal {
label = "关联"
}
hits = append(hits, hit{term: term, causal: isCausal, content: fmt.Sprintf("[%s: %s] %s\n%s", label, term, rel, text)})
break
}
}
}
return nil
})
// 优先原词命中
var results []string
for _, h := range hits {
if !h.causal {
results = append(results, h.content)
}
}
for _, h := range hits {
if h.causal {
results = append(results, h.content)
}
}
return results, err
}