728 lines
19 KiB
Go
728 lines
19 KiB
Go
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
|
||
}
|