feat: tao-mcp-go core features

This commit is contained in:
OpenClaw Agent
2026-03-14 16:09:09 +08:00
parent ebaf325222
commit a9c26fae45
7 changed files with 1604 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
tao-mcp
knowledge_ocean/
*.log
.DS_Store

11
Dockerfile Normal file
View File

@@ -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"]

182
README.md Normal file
View File

@@ -0,0 +1,182 @@
# Tao Memory MCP Server (Go)
面向 MCP 的“道场记忆”服务SSE + JSONRPC 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`
- 所有 JSONRPC 响应通过 SSE `event: message` 返回
- CORS 已放行(含 `OPTIONS`
---
## 工具清单
### 1) query_memory
全文检索(默认不搜索归档)
- `q` (string)
- `mode` (exact|causal, 默认 exact)
- `related_terms` (array<string>) 仅 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<string>, 可选;默认 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_<timestamp>.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` 叠加

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module tao-mcp
go 1.21

295
main.go Normal file
View File

@@ -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, &params)
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))
}

471
mcp_tools.go Normal file
View File

@@ -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: "在知识海洋中进行全文检索(默认 exactcausal 时可传 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
}

638
tao_core.go Normal file
View File

@@ -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
}