Skip to content

feat: add memory template customization feature#592

Open
toarujs wants to merge 1 commit into
chaitin:mainfrom
toarujs:main
Open

feat: add memory template customization feature#592
toarujs wants to merge 1 commit into
chaitin:mainfrom
toarujs:main

Conversation

@toarujs
Copy link
Copy Markdown

@toarujs toarujs commented May 13, 2026

Memory 模板功能集成 - 变更文档

概述

在 MonkeyCode 平台中添加"Memory 模板"自定义功能,允许用户在设置中自定义 .monkeycode/MEMORY.md 文件的初始化模板。


后端修改

1. 数据库 Schema

文件: backend/ent/schema/user.go

修改内容:

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        // ... 原有字段
        field.String("memory_template").Optional().Nillable(),  // <-- 新增
        // ...
    }
}

说明: 在 User 表中添加 memory_template 字段,类型为字符串,可选,可空。


2. 领域模型

文件: backend/domain/user.go

修改 User 结构体

type User struct {
    ID             uuid.UUID         `json:"id"`
    Name           string            `json:"name"`
    AvatarURL      string            `json:"avatar_url"`
    Email          string            `json:"email"`
    Role           consts.UserRole   `json:"role"`
    Status         consts.UserStatus `json:"status"`
    IsBlocked      bool              `json:"is_blocked"`
    Token          string            `json:"token,omitempty"`
    Identities     []*UserIdentity   `json:"identities"`
    Team           *Team             `json:"team,omitempty"`
    HasPassword    bool              `json:"has_password"`
    MemoryTemplate *string           `json:"memory_template,omitempty"`  // <-- 新增
}

修改 From 方法

func (u *User) From(src *db.User) *User {
    // ... 原有字段映射
    u.MemoryTemplate = src.MemoryTemplate  // <-- 新增
    // ...
}

修改 UpdateUserReq 结构体

type UpdateUserReq struct {
    Name           string  `json:"name,omitempty" form:"name"`
    AvatarURL      string  `json:"avatar_url,omitempty" form:"avatar_url"`
    MemoryTemplate *string `json:"memory_template,omitempty" form:"memory_template"`  // <-- 新增
}

修改 UserRepo 接口

type UserRepo interface {
    Get(ctx context.Context, uid uuid.UUID) (*db.User, error)
    Update(ctx context.Context, uid uuid.UUID, name, avatarURL string) error
    UpdateMemoryTemplate(ctx context.Context, uid uuid.UUID, memoryTemplate string) error  // <-- 新增
    GetUserWithTeams(ctx context.Context, uid uuid.UUID) (*db.User, error)
    PasswordLogin(ctx context.Context, req *TeamLoginReq) (*db.User, error)
    ChangePassword(ctx context.Context, uid uuid.UUID, currentPassword, newPassword string, isReset bool) error
    GetUserByEmail(ctx context.Context, emails []string) ([]*db.User, error)
    SetEmail(ctx context.Context, userID uuid.UUID, email string) error
}

3. 仓库层

文件: backend/biz/user/repo/user.go

新增方法:

// UpdateMemoryTemplate implements domain.UserRepo.
func (u *userRepo) UpdateMemoryTemplate(ctx context.Context, uid uuid.UUID, memoryTemplate string) error {
    return u.db.User.UpdateOneID(uid).
        SetMemoryTemplate(memoryTemplate).
        Exec(ctx)
}

4. 业务逻辑层

文件: backend/biz/user/usecase/user.go

修改 Update 方法:

// Update implements domain.UserUsecase.
func (u *UserUsecase) Update(ctx context.Context, uid uuid.UUID, avatarURL string, req domain.UpdateUserReq) (*domain.User, error) {
    // 如果有 memory_template,更新它
    if req.MemoryTemplate != nil {
        err := u.repo.UpdateMemoryTemplate(ctx, uid, *req.MemoryTemplate)
        if err != nil {
            u.logger.ErrorContext(ctx, "update memory template failed", "error", err, "user_id", uid)
            return nil, err
        }
    }

    // 更新其他字段
    if req.Name != "" || avatarURL != "" {
        err := u.repo.Update(ctx, uid, req.Name, avatarURL)
        if err != nil {
            u.logger.ErrorContext(ctx, "update user failed", "error", err, "user_id", uid)
            return nil, err
        }
    }

    user, err := u.Get(ctx, uid)
    if err != nil {
        u.logger.ErrorContext(ctx, "get updated user failed", "error", err, "user_id", uid)
        return nil, err
    }
    return user, nil
}

5. API Handler(新增)

文件: backend/biz/setting/handler/v1/memory_template.go

package v1

import (
    "log/slog"
    "net/http"

    "github.com/GoYoko/web"
    "github.com/samber/do"

    "github.com/chaitin/MonkeyCode/backend/domain"
    "github.com/chaitin/MonkeyCode/backend/errcode"
    "github.com/chaitin/MonkeyCode/backend/middleware"
)

// MemoryTemplateHandler Memory模板处理器
type MemoryTemplateHandler struct {
    usecase domain.UserUsecase
    logger  *slog.Logger
}

// NewMemoryTemplateHandler 创建Memory模板处理器
func NewMemoryTemplateHandler(i *do.Injector) (*MemoryTemplateHandler, error) {
    w := do.MustInvoke[*web.Web](i)
    logger := do.MustInvoke[*slog.Logger](i)
    usecase := do.MustInvoke[domain.UserUsecase](i)
    auth := do.MustInvoke[*middleware.AuthMiddleware](i)
    targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

    h := &MemoryTemplateHandler{
        logger:  logger.With("component", "handler.memory-template"),
        usecase: usecase,
    }

    v1 := w.Group("/api/v1/users/settings")

    v1.Use(auth.Auth(), targetActive.TargetActive())
    v1.GET("/memory-template", web.BindHandler(h.Get))
    v1.PUT("/memory-template", web.BindHandler(h.Update))
    v1.DELETE("/memory-template", web.BindHandler(h.Delete))

    return h, nil
}

// GetMemoryTemplateReq 获取Memory模板请求
type GetMemoryTemplateReq struct{}

// Get 获取用户Memory模板
func (h *MemoryTemplateHandler) Get(c *web.Context, req GetMemoryTemplateReq) error {
    user := middleware.GetUser(c)

    u, err := h.usecase.Get(c.Request().Context(), user.ID)
    if err != nil {
        h.logger.ErrorContext(c.Request().Context(), "failed to get user memory template", "error", err, "user_id", user.ID)
        return errcode.ErrDatabaseQuery.Wrap(err)
    }

    if u == nil || u.MemoryTemplate == nil {
        return c.Success("")
    }

    return c.Success(*u.MemoryTemplate)
}

// UpdateMemoryTemplateReq 更新Memory模板请求
type UpdateMemoryTemplateReq struct {
    MemoryTemplate string `json:"memory_template"`
}

// Update 更新用户Memory模板
func (h *MemoryTemplateHandler) Update(c *web.Context, req UpdateMemoryTemplateReq) error {
    user := middleware.GetUser(c)

    // 验证模板大小(最大 500KB)
    if len(req.MemoryTemplate) > 500*1024 {
        return c.JSON(http.StatusBadRequest, map[string]interface{}{
            "code":    400,
            "message": "模板大小超过限制(最大500KB)",
        })
    }

    _, err := h.usecase.Update(c.Request().Context(), user.ID, "", domain.UpdateUserReq{
        MemoryTemplate: &req.MemoryTemplate,
    })
    if err != nil {
        h.logger.ErrorContext(c.Request().Context(), "failed to update user memory template", "error", err, "user_id", user.ID)
        return errcode.ErrDatabaseOperation.Wrap(err)
    }

    return c.Success(nil)
}

// DeleteMemoryTemplateReq 删除Memory模板请求
type DeleteMemoryTemplateReq struct{}

// Delete 删除用户Memory模板(恢复默认)
func (h *MemoryTemplateHandler) Delete(c *web.Context, req DeleteMemoryTemplateReq) error {
    user := middleware.GetUser(c)

    emptyTemplate := ""
    _, err := h.usecase.Update(c.Request().Context(), user.ID, "", domain.UpdateUserReq{
        MemoryTemplate: &emptyTemplate,
    })
    if err != nil {
        h.logger.ErrorContext(c.Request().Context(), "failed to delete user memory template", "error", err, "user_id", user.ID)
        return errcode.ErrDatabaseOperation.Wrap(err)
    }

    return c.Success(nil)
}

6. 注册

文件: backend/biz/setting/register.go

修改内容:

// ProvideSetting 注册 setting 模块的服务工厂
func ProvideSetting(i *do.Injector) {
    // ... 原有注册
    do.Provide(i, v1.NewMemoryTemplateHandler)  // <-- 新增
}

// InvokeSetting 触发 setting 模块的 handler 初始化
func InvokeSetting(i *do.Injector) {
    // ... 原有初始化
    do.MustInvoke[*v1.MemoryTemplateHandler](i)  // <-- 新增
}

7. 自动生成的 Ent 代码

以下文件由 Ent 代码生成工具自动生成,无需手动修改:

  • backend/db/user.go - User 实体添加 MemoryTemplate 字段
  • backend/db/user/user.go - 添加 FieldMemoryTemplate 常量
  • backend/db/user/where.go - 添加 MemoryTemplate 查询条件
  • backend/db/user_create.go - 添加创建时设置 MemoryTemplate
  • backend/db/user_update.go - 添加更新 MemoryTemplate 方法
  • backend/db/mutation.go - 添加 Mutation 方法
  • backend/db/runtime/runtime.go - 运行时配置
  • backend/db/migrate/schema.go - 数据库迁移配置

前端修改

1. 新增组件

文件: frontend/src/components/console/settings/memory-template.tsx

import { useState, useEffect } from "react"
import { FileText, Save, RotateCcw, AlertTriangle } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { toast } from "sonner"

const DEFAULT_TEMPLATE = `# 用户指令记忆

本文件记录了用户的指令、偏好和教导,用于在未来的交互中提供参考。

## 格式

### 用户指令条目
用户指令条目应遵循以下格式:

[用户指令摘要]
- Date: {{date}}
- Context: [提及的场景或时间]
- Instructions:
  - [用户教导或指示的内容,逐行描述]

### 项目知识条目
Agent 在任务执行过程中发现的条目应遵循以下格式:

[项目知识摘要]
- Date: {{date}}
- Context: Agent 在执行 [具体任务描述] 时发现
- Category: [代码结构|代码模式|代码生成|构建方法|测试方法|依赖关系|环境配置]
- Instructions:
  - [具体的知识点,逐行描述]

## 去重策略
- 添加新条目前,检查是否存在相似或相同的指令
- 若发现重复,跳过新条目或与已有条目合并
- 合并时,更新上下文或日期信息
- 这有助于避免冗余条目,保持记忆文件整洁

## 条目

[按上述格式记录的记忆条目]
`

const VARIABLES = [
  { name: "date", desc: "当前日期" },
  { name: "datetime", desc: "当前日期时间" },
  { name: "project_name", desc: "项目名称" },
  { name: "user_name", desc: "用户名" },
  { name: "workspace_path", desc: "工作空间路径" },
]

export default function MemoryTemplate() {
  const [template, setTemplate] = useState("")
  const [originalTemplate, setOriginalTemplate] = useState("")
  const [loading, setLoading] = useState(false)
  const [showResetConfirm, setShowResetConfirm] = useState(false)
  const [hasCustomTemplate, setHasCustomTemplate] = useState(false)

  useEffect(() => {
    fetchTemplate()
  }, [])

  const fetchTemplate = async () => {
    try {
      const response = await fetch("/api/v1/users/settings/memory-template", {
        headers: {
          "Content-Type": "application/json",
        },
        credentials: "include",
      })
      
      if (response.ok) {
        const data = await response.json()
        if (data.data) {
          setTemplate(data.data)
          setOriginalTemplate(data.data)
          setHasCustomTemplate(true)
        } else {
          setTemplate(DEFAULT_TEMPLATE)
          setOriginalTemplate(DEFAULT_TEMPLATE)
          setHasCustomTemplate(false)
        }
      } else {
        setTemplate(DEFAULT_TEMPLATE)
        setOriginalTemplate(DEFAULT_TEMPLATE)
      }
    } catch (error) {
      console.error("Error fetching template:", error)
      setTemplate(DEFAULT_TEMPLATE)
      setOriginalTemplate(DEFAULT_TEMPLATE)
    }
  }

  const handleSave = async () => {
    setLoading(true)
    try {
      const response = await fetch("/api/v1/users/settings/memory-template", {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
        credentials: "include",
        body: JSON.stringify({ memory_template: template }),
      })

      if (response.ok) {
        setOriginalTemplate(template)
        setHasCustomTemplate(true)
        toast.success("模板保存成功")
      } else {
        const error = await response.json()
        toast.error(error.message || "保存失败")
      }
    } catch (error) {
      console.error("Error saving template:", error)
      toast.error("保存失败,请重试")
    } finally {
      setLoading(false)
    }
  }

  const handleReset = async () => {
    setLoading(true)
    try {
      const response = await fetch("/api/v1/users/settings/memory-template", {
        method: "DELETE",
        credentials: "include",
      })

      if (response.ok) {
        setTemplate(DEFAULT_TEMPLATE)
        setOriginalTemplate(DEFAULT_TEMPLATE)
        setHasCustomTemplate(false)
        toast.success("已恢复为默认模板")
      } else {
        toast.error("恢复失败")
      }
    } catch (error) {
      console.error("Error resetting template:", error)
      toast.error("恢复失败")
    } finally {
      setLoading(false)
      setShowResetConfirm(false)
    }
  }

  const hasChanges = template !== originalTemplate

  return (
    <div className="h-full flex flex-col">
      <div className="flex-shrink-0 mb-4">
        <h2 className="text-lg font-semibold">Memory 模板</h2>
        <p className="text-sm text-muted-foreground">
          自定义 MEMORY.md 初始化模板,新项目创建时将使用此模板
        </p>
      </div>

      <div className="flex-1 flex flex-col min-h-0 rounded-lg border p-4">
        <div className="flex-shrink-0 flex items-center justify-between mb-3">
          <div className="space-y-1">
            <label className="text-sm font-medium">模板内容</label>
            <p className="text-xs text-muted-foreground">
              支持以下变量:
              {VARIABLES.map((v) => (
                <code key={v.name} className="mx-1 px-1 py-0.5 bg-muted rounded text-xs">
                  {`{{${v.name}}}`}
                </code>
              ))}
            </p>
          </div>
          <div className="flex items-center gap-2">
            {hasCustomTemplate && (
              <span className="text-xs text-muted-foreground flex items-center gap-1">
                <FileText className="size-3" />
                已自定义
              </span>
            )}
          </div>
        </div>

        <div className="flex-1 min-h-0 mb-4">
          <Textarea
            value={template}
            onChange={(e) => setTemplate(e.target.value)}
            className="w-full h-full min-h-[200px] font-mono text-sm resize-none"
            placeholder="输入自定义模板..."
            disabled={loading}
          />
        </div>

        <div className="flex-shrink-0 flex items-center justify-between">
          <div className="flex items-center gap-2">
            <Button
              onClick={handleSave}
              disabled={loading || !hasChanges}
              size="sm"
            >
              <Save className="size-4 mr-1" />
              {loading ? "保存中..." : "保存模板"}
            </Button>
            
            <Button
              variant="outline"
              onClick={() => setShowResetConfirm(true)}
              disabled={loading || !hasCustomTemplate}
              size="sm"
            >
              <RotateCcw className="size-4 mr-1" />
              恢复默认
            </Button>
          </div>

          {hasChanges && (
            <span className="text-xs text-amber-600 flex items-center gap-1">
              <AlertTriangle className="size-3" />
              有未保存的更改
            </span>
          )}
        </div>
      </div>

      <AlertDialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>恢复默认模板</AlertDialogTitle>
            <AlertDialogDescription>
              确定要恢复为默认模板吗?这将删除您自定义的模板,此操作不可撤销。
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel onClick={() => setShowResetConfirm(false)}>
              取消
            </AlertDialogCancel>
            <AlertDialogAction onClick={handleReset} className="bg-destructive">
              恢复默认
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  )
}

2. 设置对话框

文件: frontend/src/components/console/settings/settings-dialog.tsx

导入部分

import { FileText } from "lucide-react"  // <-- 新增
import MemoryTemplate from "./memory-template"  // <-- 新增

导航配置

const SETTINGS_NAV = [
    { id: "identities", name: "Git 身份", icon: IconPasswordFingerprint },
    { id: "tools-mcp", name: "MCP 与工具", icon: Blocks },
    { id: "models", name: "AI 大模型", icon: Bot },
    { id: "images", name: "系统镜像", icon: Box },
    { id: "hosts", name: "宿主机", icon: HardDrive },
    { id: "vms", name: "开发环境", icon: MonitorCloud },
    { id: "memory-template", name: "Memory 模板", icon: FileText },  // <-- 新增
    { id: "notifications", name: "通知", icon: Bell },
] as const

内容路由

function SettingsContent({ section }: { section: SettingsSectionId }) {
    switch (section) {
        // ... 其他 case
        case "memory-template":
            return <MemoryTemplate />  // <-- 新增
        // ...
    }
}

修复滚动

// 修改前
<main className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
    <div className="flex min-h-0 flex-1 flex-col overflow-hidden p-4">

// 修改后
<main className="flex min-h-0 min-w-0 flex-1 flex-col overflow-auto">
    <div className="flex min-h-0 flex-1 flex-col overflow-auto p-4">

API 接口文档

获取 Memory 模板

接口: GET /api/v1/users/settings/memory-template

响应:

{
    "code": 200,
    "data": "模板内容...",
    "message": "ok"
}

说明: 如果用户未设置模板,返回空字符串。


保存 Memory 模板

接口: PUT /api/v1/users/settings/memory-template

请求体:

{
    "memory_template": "模板内容..."
}

响应:

{
    "code": 200,
    "data": null,
    "message": "ok"
}

限制: 模板大小最大 500KB。


删除 Memory 模板

接口: DELETE /api/v1/users/settings/memory-template

响应:

{
    "code": 200,
    "data": null,
    "message": "ok"
}

说明: 删除后恢复为系统默认模板。


模板变量

变量 说明 示例
{{date}} 当前日期 2026-05-13
{{datetime}} 当前日期时间 2026-05-13 04:33:00
{{project_name}} 项目名称 my-project
{{user_name}} 用户名 john-doe
{{workspace_path}} 工作空间路径 /workspace

数据流

用户编辑模板 → 前端 PUT /api/v1/users/settings/memory-template
                    ↓
              后端 MemoryTemplateHandler.Update
                    ↓
              UserUsecase.Update (检查 MemoryTemplate 字段)
                    ↓
              UserRepo.UpdateMemoryTemplate
                    ↓
              数据库 users.memory_template 字段

测试步骤

  1. 启动后端服务
  2. 打开前端页面,进入设置
  3. 点击左侧导航"Memory 模板"
  4. 编辑模板内容,点击"保存模板"
  5. 刷新页面,验证模板是否保存成功
  6. 点击"恢复默认",验证是否恢复为默认模板

注意事项

  1. 数据库迁移: 部署时需要执行数据库迁移,添加 memory_template 字段
  2. 模板大小: 后端限制模板大小最大 500KB
  3. 安全性: 建议在前端对模板内容进行 XSS 过滤
  4. 向后兼容: 未设置模板的用户使用系统默认模板

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant