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