feat: tao-mcp-go core features
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
tao-mcp
|
||||
knowledge_ocean/
|
||||
*.log
|
||||
.DS_Store
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal 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
182
README.md
Normal file
@@ -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<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` 叠加
|
||||
295
main.go
Normal file
295
main.go
Normal 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, ¶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))
|
||||
}
|
||||
471
mcp_tools.go
Normal file
471
mcp_tools.go
Normal 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: "在知识海洋中进行全文检索(默认 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
|
||||
}
|
||||
638
tao_core.go
Normal file
638
tao_core.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user