diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4416a09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +tao-mcp +knowledge_ocean/ +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e89cdb4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.21-alpine AS builder +WORKDIR /app +COPY . . +RUN go build -o tao-mcp . + +FROM alpine:latest +WORKDIR /app +COPY --from=builder /app/tao-mcp . +VOLUME ["/app/knowledge_ocean"] +EXPOSE 5001 +CMD ["./tao-mcp"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a19cfb --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# Tao Memory MCP Server (Go) + +面向 MCP 的“道场记忆”服务:SSE + JSON‑RPC 2.0,支持移动端、异步响应、灵感/日记/周月季半年年炼化与归档剪枝。 + +--- + +## 功能总览 + +- **MCP 协议**:SSE + `/mcp/message` 异步响应(POST 返回 202) +- **鉴权**:Header Bearer 或 `?token=` +- **工具体系**:检索、灵感、日记、周/月/季/半年/年总结、归档剪枝、代码巡检提案 +- **存储结构**:六级流转(YEAR/H1|H2/Q/Month/Wxx) +- **归档剪枝**:月度归档至 `_Archive`,检索默认跳过归档 + +--- + +## 目录结构(六级流转) + +``` +knowledge_ocean/ + 2026/ + H1_Upper/ + Q1/ + 03_March/ + W11/ + 2026-03-14_Saturday.md + Week_Summary.md + Month_Summary.md + Semiannual_Summary.md + H2_Lower/ + ... + Year_Summary.md + Inspirations/ + Idea_1710400000.md + _Archive/ + 2026/03/... + _Proposals/ + proposal_20260314_150405.md +``` + +--- + +## MCP 端点 + +- **SSE**:`/mcp/sse?token=...` +- **消息**:`/mcp/message?token=...` + +**特性:** +- `POST /mcp/message` 只返回 `202 Accepted` +- 所有 JSON‑RPC 响应通过 SSE `event: message` 返回 +- CORS 已放行(含 `OPTIONS`) + +--- + +## 工具清单 + +### 1) query_memory +全文检索(默认不搜索归档) + +- `q` (string) +- `mode` (exact|causal, 默认 exact) +- `related_terms` (array) 仅 mode=causal +- `include_archive` (bool, 默认 false) + +**causal 输出标注:** `[命中: 关键词]` / `[关联: 关键词]` + +--- + +### 2) get_week_data / record_summary +- `get_week_data(weekOffset)`:获取周素材 +- `record_summary(content, weekOffset)`:写入 `Week_Summary.md` + - 若缺少 `### 📅 时空坐标`,自动套“炼丹笔记”模板 + +--- + +### 3) capture_idea +捕获灵感并**双写**: +- 写入 `Inspirations/Idea_*.md`(YAML Frontmatter + tags) +- 同时写入当天日记(category=idea) + +参数: +- `content` (string) +- `tags` (array, 可选;默认 Unsorted) + +--- + +### 4) record_daily +写入当日日记(不会进入灵感库) + +参数: +- `content` (string) +- `karma` (int, 默认 1) + +--- + +### 5) 月/季/半年/年炼化 +- `get_month_data` / `record_month_summary` +- `get_quarter_data` / `record_quarter_summary` +- `get_semiannual_data` / `record_semiannual_summary` +- `get_year_data` / `record_year_summary` + +产物: +- `Month_Summary.md` +- `Quarter_Summary.md` +- `Semiannual_Summary.md` +- `Year_Summary.md` + +--- + +### 6) housekeep_memory +归档剪枝(按月) + +参数: +- `target_month` (YYYY-MM) + +逻辑: +- 校验 `Month_Summary.md` 存在且大小≥100字节且包含 `###` +- 将该月 `Wxx/` 目录及非 Month_Summary 文件移动到 `_Archive/YYYY/MM/` +- 生成 `ARCHIVED.txt` +- `query_memory` 默认跳过 `_Archive` + +--- + +### 7) inspect_and_propose +检视代码与灵感,生成补丁建议清单(不自动应用) + +参数: +- `repo_path` (可选,默认 `/root/.openclaw/workspace/tao_mcp_go`) + +行为: +- `git pull`(失败不阻断) +- 扫描 `Inspirations` 中 `#Todo/#Fix` +- 生成 `_Proposals/proposal_.md` +- 当日日记记录摘要 + +--- + +## OpenClaw 接入(示例) + +**Base URL** +``` +https://mcp.good.xx.kg +``` + +**Auth Token** +``` +a3c60a86ed2a7d317b8855faa94a05d1 +``` + +**Instructions(粘贴)** +``` +你接入的 MCP 服务器名为 Tao-Memory-Pro。 +调用工具时遵循以下规则: +1) 重要灵感/架构设计 → 使用 capture_idea(可带 tags) +2) 日常记录/普通对话 → 使用 record_daily +3) 周总结使用 record_summary(自动套用炼丹模板) +4) 检索默认 query_memory;需要查归档时 include_archive=true +``` + +--- + +## 编译与运行 + +```bash +cd /root/.openclaw/workspace/tao_mcp_go + +go build -o tao-mcp +./tao-mcp +``` + +## Systemd +```bash +systemctl restart tao-mcp-go +systemctl status tao-mcp-go +``` + +--- + +## 备注 +- SSE 已设置 `X-Accel-Buffering: no` +- MCP 协议版本:`2025-06-18` +- `TAO_ENDPOINT_STYLE=message` 避免 `/mcp/mcp` 叠加 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d20f8e5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module tao-mcp + +go 1.21 diff --git a/main.go b/main.go new file mode 100644 index 0000000..4bbf4bd --- /dev/null +++ b/main.go @@ -0,0 +1,295 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "time" +) + +// --- MCP Protocol Types --- +type MCPRequest struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type MCPResponse struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id,omitempty"` + Result any `json:"result,omitempty"` + Error interface{} `json:"error,omitempty"` +} + +type MCPContent struct { + Type string `json:"type"` + Text string `json:"text"` +} + +func sendMCPResponse(id any, result any) MCPResponse { + return MCPResponse{JSONRPC: "2.0", ID: id, Result: result} +} + +func (s *TaoServer) dispatchMCP(token string, req MCPRequest) { + var resp MCPResponse + resp.JSONRPC = "2.0" + resp.ID = req.ID + + switch req.Method { + case "initialize": + resp.Result = map[string]interface{}{ + "protocolVersion": "2025-06-18", + "capabilities": map[string]interface{}{ + "tools": map[string]interface{}{"listChanged": false}, + "resources": map[string]interface{}{"listChanged": false}, + "prompts": map[string]interface{}{"listChanged": false}, + "logging": map[string]interface{}{}, + }, + "serverInfo": map[string]string{ + "name": "Tao-Memory-Server", + "version": "1.2.0", + }, + } + case "notifications/initialized": + log.Printf("[MCP Notify] initialized from %s", token) + return + case "tools/list": + resp.Result = map[string]interface{}{ + "tools": buildToolList(), + } + case "tools/call": + var params struct { + Name string `json:"name"` + Arguments map[string]interface{} `json:"arguments"` + } + _ = json.Unmarshal(req.Params, ¶ms) + if tool, ok := ToolRegistry[params.Name]; ok { + result, err := tool.Handler(params.Arguments) + if err != nil { + resp.Result = map[string]interface{}{ + "content": []MCPContent{{Type: "text", Text: "error: " + err.Error()}}, + } + } else { + resp.Result = map[string]interface{}{ + "content": []MCPContent{{Type: "text", Text: result}}, + } + } + } else { + resp.Result = map[string]interface{}{ + "content": []MCPContent{{Type: "text", Text: "error: tool not found"}}, + } + } + default: + _ = s.Record("agent_action", fmt.Sprintf("执行指令: %+v", req), 2) + return + } + + if token == "" { + log.Printf("[MCP Response] missing token for method=%s", req.Method) + return + } + if ch, ok := s.conns.Load(token); ok { + if b, err := json.Marshal(resp); err == nil { + ch.(chan string) <- string(b) + log.Printf("[MCP Response] sent via SSE method=%s", req.Method) + } + } else { + log.Printf("[MCP Response] no SSE channel for token=%s method=%s", token, req.Method) + } +} + +func getEnv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +// --- 以简御繁:鉴权 --- +func (s *TaoServer) checkAuth(r *http.Request) bool { + token := getEnv("TAO_AUTH_TOKEN", "") + if token == "" { + return true // 未配置则不启用鉴权 + } + // Header Bearer + h := r.Header.Get("Authorization") + if h == "Bearer "+token { + return true + } + // Query token + if q := r.URL.Query().Get("token"); q != "" && q == token { + return true + } + return false +} + +func (s *TaoServer) requireAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.WriteHeader(http.StatusOK) + return + } + if !s.checkAuth(r) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next(w, r) + } +} + +// --- 感官 (Webhook Adapters) --- +// 适配 Gitea 的 Push Webhook +func (s *TaoServer) GiteaHandler(w http.ResponseWriter, r *http.Request) { + var payload struct { + Repository struct { + Name string `json:"name"` + } `json:"repository"` + Commits []struct { + Message string `json:"message"` + } `json:"commits"` + } + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Bad Request", 400) + return + } + + if len(payload.Commits) > 0 { + msg := payload.Commits[0].Message + summary := fmt.Sprintf("代码演化于 [%s]: %s", payload.Repository.Name, msg) + _ = s.Record("code", summary, 4) + } + w.WriteHeader(200) +} + +// 适配 SmsReceiver-go 的短信推送 +func (s *TaoServer) SmsHandler(w http.ResponseWriter, r *http.Request) { + var payload struct { + From string `json:"from"` + Content string `json:"content"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Bad Request", 400) + return + } + + summary := fmt.Sprintf("收到信号 [%s]: %s", payload.From, payload.Content) + _ = s.Record("sms", summary, 3) + w.WriteHeader(200) +} + +// --- MCP SSE --- +func (s *TaoServer) SSEHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("[SSE Connect] Remote=%s URL=%s", r.RemoteAddr, r.URL.String()) + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("X-Accel-Buffering", "no") + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + + // 告知客户端 POST 入口(按客户端拼接习惯输出) + style := getEnv("TAO_ENDPOINT_STYLE", "message") + endpoint := "mcp/message" + if style == "message" { + endpoint = "message" + } + // 若通过 query token 访问,也把 token 拼到 endpoint(便于客户端无 Header) + token := r.URL.Query().Get("token") + if token != "" { + if strings.Contains(endpoint, "?") { + endpoint = endpoint + "&token=" + token + } else { + endpoint = endpoint + "?token=" + token + } + } + fmt.Fprintf(w, "event: endpoint\ndata: %s\n\n", endpoint) + flusher.Flush() + + var msgChan chan string + if token != "" { + msgChan = make(chan string, 50) + s.conns.Store(token, msgChan) + defer s.conns.Delete(token) + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-r.Context().Done(): + return + case <-ticker.C: + fmt.Fprintf(w, ":ping\n\n") + flusher.Flush() + case msg := <-msgChan: + fmt.Fprintf(w, "event: message\ndata: %s\n\n", msg) + flusher.Flush() + } + } +} + +// --- MCP Message --- +func (s *TaoServer) MessageHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + bodyBytes, _ := io.ReadAll(r.Body) + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + log.Printf("[MCP POST] From=%s URL=%s Body=%s", r.RemoteAddr, r.URL.String(), string(bodyBytes)) + + var req MCPRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Bad Request", 400) + return + } + + token := r.URL.Query().Get("token") + w.WriteHeader(http.StatusAccepted) + + go s.dispatchMCP(token, req) +} + +// --- 主程序 (Main) --- +func main() { + server := &TaoServer{ + config: Config{ + MemoryRoot: getEnv("MEMORY_ROOT", "./knowledge_ocean"), + Port: getEnv("PORT", "5001"), + }, + } + + server.RegisterTools() + + // 启动 Webhook 监听 (感知层) + http.HandleFunc("/ingest/gitea", server.requireAuth(server.GiteaHandler)) + http.HandleFunc("/ingest/sms", server.requireAuth(server.SmsHandler)) + + // MCP SSE + Message + http.HandleFunc("/mcp/sse", server.requireAuth(server.SSEHandler)) + http.HandleFunc("/mcp/message", server.requireAuth(server.MessageHandler)) + + fmt.Printf("Tao Memory Server 启动。道场地址: :%s\n", server.config.Port) + log.Fatal(http.ListenAndServe(":"+server.config.Port, nil)) +} diff --git a/mcp_tools.go b/mcp_tools.go new file mode 100644 index 0000000..a8de10a --- /dev/null +++ b/mcp_tools.go @@ -0,0 +1,471 @@ +package main + +import ( + "strconv" +) + +// Tool 定义了 MCP 工具的元数据和执行逻辑 +type Tool struct { + Name string + Description string + InputSchema map[string]interface{} + Handler func(args map[string]interface{}) (string, error) +} + +// ToolRegistry 存储所有已注册的工具 +var ToolRegistry = make(map[string]Tool) + +func (s *TaoServer) RegisterTools() { + // 1) query_memory + ToolRegistry["query_memory"] = Tool{ + Name: "query_memory", + Description: "在知识海洋中进行全文检索(默认 exact;causal 时可传 related_terms 作为关联词数组;默认不搜索归档区,需 include_archive=true 才会搜索 _Archive)", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "q": map[string]interface{}{"type": "string", "description": "关键词"}, + "mode": map[string]interface{}{"type": "string", "enum": []string{"exact", "causal"}, "default": "exact"}, + "related_terms": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "关联词数组(仅 mode=causal)"}, + "include_archive": map[string]interface{}{"type": "boolean", "default": false}, + }, + "required": []string{"q"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + q, _ := args["q"].(string) + includeArchive := false + if v, ok := args["include_archive"]; ok { + if b, ok := v.(bool); ok { + includeArchive = b + } + } + mode := "exact" + if v, ok := args["mode"].(string); ok && v != "" { + mode = v + } + related := []string{} + if v, ok := args["related_terms"]; ok { + if arr, ok := v.([]interface{}); ok { + for _, t := range arr { + if s, ok := t.(string); ok && s != "" { + related = append(related, s) + } + } + } + } + results, _ := s.SearchMemoryAdvanced(q, related, mode == "causal", includeArchive) + return stringsJoin(results, "\n\n"), nil + }, + } + + // 2) get_week_data + ToolRegistry["get_week_data"] = Tool{ + Name: "get_week_data", + Description: "获取指定周的所有原始素材用于炼化", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "weekOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + }, + Handler: func(args map[string]interface{}) (string, error) { + offset := 0 + if val, ok := args["weekOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.GetWeekData(offset) + }, + } + + // 3) record_summary + ToolRegistry["record_summary"] = Tool{ + Name: "record_summary", + Description: "将炼化后的周精华存入 Week_Summary.md(若缺失‘炼丹笔记’结构将自动套用模板)", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string"}, + "weekOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + "required": []string{"content"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + content, _ := args["content"].(string) + offset := 0 + if val, ok := args["weekOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + content = s.ApplyWeeklyTemplateIfMissing(content, offset) + return s.RecordSummary(content, offset) + }, + } + + // 4) capture_idea + ToolRegistry["capture_idea"] = Tool{ + Name: "capture_idea", + Description: "捕获瞬时灵感/知识片段,自动归档并可附标签", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string", "description": "灵感内容"}, + "tags": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "可选标签"}, + }, + "required": []string{"content"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + content, _ := args["content"].(string) + tags := []string{} + if v, ok := args["tags"]; ok { + if arr, ok := v.([]interface{}); ok { + for _, t := range arr { + if s, ok := t.(string); ok && s != "" { + tags = append(tags, s) + } + } + } + } + return s.CaptureIdea(content, tags) + }, + } + + // 5) get_month_data + ToolRegistry["get_month_data"] = Tool{ + Name: "get_month_data", + Description: "获取指定月份的周总结素材", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "monthOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + }, + Handler: func(args map[string]interface{}) (string, error) { + offset := 0 + if val, ok := args["monthOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.GetMonthData(offset) + }, + } + + // 6) record_month_summary + ToolRegistry["record_month_summary"] = Tool{ + Name: "record_month_summary", + Description: "写入月总结 Month_Summary.md", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string"}, + "monthOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + "required": []string{"content"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + content, _ := args["content"].(string) + offset := 0 + if val, ok := args["monthOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.RecordMonthSummary(content, offset) + }, + } + + // 7) get_quarter_data + ToolRegistry["get_quarter_data"] = Tool{ + Name: "get_quarter_data", + Description: "获取指定季度的月总结素材", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "quarterOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + }, + Handler: func(args map[string]interface{}) (string, error) { + offset := 0 + if val, ok := args["quarterOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.GetQuarterData(offset) + }, + } + + // 8) record_quarter_summary + ToolRegistry["record_quarter_summary"] = Tool{ + Name: "record_quarter_summary", + Description: "写入季总结 Quarter_Summary.md", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string"}, + "quarterOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + "required": []string{"content"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + content, _ := args["content"].(string) + offset := 0 + if val, ok := args["quarterOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.RecordQuarterSummary(content, offset) + }, + } + + // 9) get_semiannual_data + ToolRegistry["get_semiannual_data"] = Tool{ + Name: "get_semiannual_data", + Description: "获取指定半年的季度总结素材", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "halfOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + }, + Handler: func(args map[string]interface{}) (string, error) { + offset := 0 + if val, ok := args["halfOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.GetSemiannualData(offset) + }, + } + + // 10) record_semiannual_summary + ToolRegistry["record_semiannual_summary"] = Tool{ + Name: "record_semiannual_summary", + Description: "写入半年总结 Semiannual_Summary.md", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string"}, + "halfOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + "required": []string{"content"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + content, _ := args["content"].(string) + offset := 0 + if val, ok := args["halfOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.RecordSemiannualSummary(content, offset) + }, + } + + // 11) get_year_data + ToolRegistry["get_year_data"] = Tool{ + Name: "get_year_data", + Description: "获取指定年度的半年总结素材", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "yearOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + }, + Handler: func(args map[string]interface{}) (string, error) { + offset := 0 + if val, ok := args["yearOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.GetYearData(offset) + }, + } + + // 12) record_year_summary + ToolRegistry["record_year_summary"] = Tool{ + Name: "record_year_summary", + Description: "写入年度总结 Year_Summary.md", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string"}, + "yearOffset": map[string]interface{}{"type": "integer", "default": 0}, + }, + "required": []string{"content"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + content, _ := args["content"].(string) + offset := 0 + if val, ok := args["yearOffset"]; ok { + switch t := val.(type) { + case float64: + offset = int(t) + case int: + offset = t + case string: + if n, err := strconv.Atoi(t); err == nil { + offset = n + } + } + } + return s.RecordYearSummary(content, offset) + }, + } + + // 13) housekeep_memory + ToolRegistry["housekeep_memory"] = Tool{ + Name: "housekeep_memory", + Description: "归档指定月份的原始记录(需月总结完成)", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "target_month": map[string]interface{}{"type": "string", "description": "YYYY-MM"}, + }, + "required": []string{"target_month"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + target, _ := args["target_month"].(string) + return s.HousekeepMemory(target) + }, + } + + // 14) inspect_and_propose + ToolRegistry["inspect_and_propose"] = Tool{ + Name: "inspect_and_propose", + Description: "检视 tao_mcp_go 与灵感库,生成补丁建议清单(不自动应用)", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "repo_path": map[string]interface{}{"type": "string", "description": "仓库路径(可选,默认 tao_mcp_go)"}, + }, + }, + Handler: func(args map[string]interface{}) (string, error) { + repo, _ := args["repo_path"].(string) + return s.InspectAndPropose(repo) + }, + } + + // 15) record_daily + ToolRegistry["record_daily"] = Tool{ + Name: "record_daily", + Description: "写入当日记忆流(日记/对话碎片)", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string"}, + "karma": map[string]interface{}{"type": "integer", "default": 1}, + }, + "required": []string{"content"}, + }, + Handler: func(args map[string]interface{}) (string, error) { + content, _ := args["content"].(string) + karma := 1 + if v, ok := args["karma"]; ok { + switch t := v.(type) { + case float64: + karma = int(t) + case int: + karma = t + case string: + if n, err := strconv.Atoi(t); err == nil { + karma = n + } + } + } + return s.RecordDaily(content, karma) + }, + } +} + + +func buildToolList() []map[string]interface{} { + var toolList []map[string]interface{} + for _, t := range ToolRegistry { + toolList = append(toolList, map[string]interface{}{ + "name": t.Name, + "description": t.Description, + "inputSchema": t.InputSchema, + }) + } + return toolList +} + +func stringsJoin(items []string, sep string) string { + if len(items) == 0 { + return "" + } + out := items[0] + for i := 1; i < len(items); i++ { + out = out + sep + items[i] + } + return out +} diff --git a/tao_core.go b/tao_core.go new file mode 100644 index 0000000..189428e --- /dev/null +++ b/tao_core.go @@ -0,0 +1,638 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "time" +) + +// --- 道 (Config & State) --- +type Config struct { + MemoryRoot string + Port string +} + +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 := os.Rename(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 := os.Rename(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 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" + } + + // 1) 拉取最新代码(若失败则继续) + _ = 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 +} + +// SearchMemory 遍历所有 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) + } + } + } + + err := filepath.Walk(s.config.MemoryRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !includeArchive && info.IsDir() && strings.Contains(path, string(filepath.Separator)+"_Archive"+string(filepath.Separator)) { + return filepath.SkipDir + } + if !info.IsDir() && filepath.Ext(path) == ".md" { + 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.MemoryRoot, 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 +}