GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-26-memory-management.md
2026-05-20 21:39:12 +08:00

40 KiB
Raw Blame History

记忆管理模块实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 为 Agent 系统添加记忆管理模块包含管理页面增删改查、自定义类型、Provider 缓存池、双后端适配、Agent 工具扩展。

Architecture: 在现有 netaclaw 模块内扩展。新增 MemoryProviderRegistry 单例缓存 Provider 实例MemoryAdminService 适配 MySQL/SQLite 双后端;前端在 agent 模块下新增记忆管理页面。Agent 工具扩展支持自定义类型和 metadata。

Tech Stack: Midway.js + TypeORM + Cool Admin 8.x (后端), Vue 3 + Element Plus + Composition API (前端), better-sqlite3 (SQLite)

Spec: docs/superpowers/specs/2026-04-26-memory-management-design.md


Task 1: MemoryType Entity + 注册

Files:

  • Create: packages/backend/src/modules/netaclaw/entity/memory_type.ts

  • Modify: packages/backend/src/entities.ts

  • Step 1: 创建 MemoryType Entity

// packages/backend/src/modules/netaclaw/entity/memory_type.ts
import { BaseEntity } from '../../base/entity/base.js';
import { Column, Entity, Index } from 'typeorm';

@Entity('netaclaw_memory_type')
export class NetaClawMemoryTypeEntity extends BaseEntity {
  @Index({ unique: true })
  @Column({ comment: '类型标识', length: 50 })
  key: string;

  @Column({ comment: '显示名称', length: 100 })
  name: string;

  @Column({ comment: '类型说明', length: 500, default: '' })
  description: string;

  @Column({ comment: '图标', length: 50, nullable: true })
  icon: string;

  @Column({ comment: '是否系统内置 1=是 0=否', default: 0 })
  isSystem: number;
}
  • Step 2: 注册 Entity 到 entities.ts

packages/backend/src/entities.ts 中,先检查当前最后一个 entity 编号(当前为 entity44在其后添加新 import编号 +1

import * as entity45 from './modules/netaclaw/entity/memory_type';

entities 数组末尾添加(编号与 import 一致):

  ...Object.values(entity45),

注意:如果其他开发者已新增 entity编号可能不是 45实施时以实际文件末尾编号 +1 为准。

  • Step 3: 启动后端验证表自动创建

Run: cd C:/Users/Administrator/Desktop/code/Neta-monorepo && pnpm --filter @neta/backend dev

验证日志中出现 netaclaw_memory_type 表创建成功synchronize: true 自动建表)。验证后停止服务。

  • Step 4: 插入预置系统类型

通过 MCP MySQL 工具执行:

INSERT INTO netaclaw_memory_type (`key`, name, description, isSystem, createTime, updateTime)
VALUES
  ('user', '用户画像', '用户偏好、角色、习惯', 1, NOW(), NOW()),
  ('project', '项目知识', '项目进展、决策、约束', 1, NOW(), NOW()),
  ('feedback', '行为反馈', '用户对 Agent 行为的纠正或确认', 1, NOW(), NOW()),
  ('reference', '引用资源', '外部资源链接、文档地址', 1, NOW(), NOW());
  • Step 5: 提交
git add packages/backend/src/modules/netaclaw/entity/memory_type.ts packages/backend/src/entities.ts
git commit -m "feat(memory): add NetaClawMemoryType entity with system presets"

Task 2: MemoryType Service + Controller (标准 CRUD)

Files:

  • Create: packages/backend/src/modules/netaclaw/service/memory_type.ts

  • Create: packages/backend/src/modules/netaclaw/controller/admin/memory_type.ts

  • Step 1: 创建 MemoryType Service

// packages/backend/src/modules/netaclaw/service/memory_type.ts
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawMemoryTypeEntity } from '../entity/memory_type.js';

@Provide()
export class MemoryTypeService extends BaseService {
  @InjectEntityModel(NetaClawMemoryTypeEntity)
  memoryTypeEntity: Repository<NetaClawMemoryTypeEntity>;

  async deleteWithCheck(ids: number[]): Promise<void> {
    const systemTypes = await this.memoryTypeEntity.find({
      where: ids.map(id => ({ id, isSystem: 1 })),
    });
    if (systemTypes.length > 0) {
      const names = systemTypes.map(t => t.name).join(', ');
      throw new Error(`系统内置类型不可删除: ${names}`);
    }
    await this.memoryTypeEntity.delete(ids);
  }

  async allTypes(): Promise<NetaClawMemoryTypeEntity[]> {
    return this.memoryTypeEntity.find({ order: { isSystem: 'DESC', createTime: 'ASC' } });
  }
}
  • Step 2: 创建 MemoryType Controller
// packages/backend/src/modules/netaclaw/controller/admin/memory_type.ts
import { Provide, Inject, Post, Body } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { NetaClawMemoryTypeEntity } from '../../entity/memory_type.js';
import { MemoryTypeService } from '../../service/memory_type.js';

@Provide()
@CoolController({
  api: ['add', 'update', 'info', 'list', 'page'],
  entity: NetaClawMemoryTypeEntity,
  service: MemoryTypeService,
  pageQueryOp: {
    keyWordLikeFields: ['key', 'name'],
    fieldEq: ['isSystem'],
    addOrderBy: { isSystem: 'DESC', createTime: 'ASC' },
  },
})
export class AdminNetaClawMemoryTypeController extends BaseController {
  @Inject()
  memoryTypeService: MemoryTypeService;

  @Post('/delete', { summary: '删除类型(系统内置不可删)' })
  async delete(@Body() body: { ids: number[] }) {
    try {
      await this.memoryTypeService.deleteWithCheck(body.ids);
      return this.ok();
    } catch (e: any) {
      return this.fail(e.message);
    }
  }
}
  • Step 3: 启动后端验证接口

启动后端,用 curl 或浏览器测试:

curl -X POST http://127.0.0.1:8003/admin/netaclaw/memory_type/list -H "Authorization: Bearer <token>" -H "Content-Type: application/json"

预期返回 4 条系统类型数据。

  • Step 4: 提交
git add packages/backend/src/modules/netaclaw/service/memory_type.ts packages/backend/src/modules/netaclaw/controller/admin/memory_type.ts
git commit -m "feat(memory): add MemoryType CRUD service and controller"

Task 3: Provider 接口扩展 + userId 可选

Files:

  • Modify: packages/backend/src/modules/netaclaw/memory/provider.ts

  • Step 1: 扩展 provider.ts 接口

packages/backend/src/modules/netaclaw/memory/provider.ts 文件末尾 MemoryProvider 接口之前,添加新的类型定义,并修改 MemorySearchOptsuserId 为可选:

替换现有的 MemoryType 定义(第 5 行)和 MemorySearchOpts(第 7-12 行):

// 替换第 5 行的 MemoryType
export type MemoryType = 'user' | 'project' | 'feedback' | 'reference' | (string & {});

// 替换第 7-12 行的 MemorySearchOpts
export interface MemorySearchOpts {
  agentName: string;
  userId?: string;   // 改为可选 — 管理页面跨用户查询
  type?: MemoryType;
  limit?: number;
}

export interface MemoryPageOpts {
  agentName?: string;
  userId?: string;
  type?: string;
  keyWord?: string;
  page: number;
  size: number;
}

export interface MemoryPageResult {
  list: MemoryEntry[];
  pagination: { page: number; size: number; total: number };
}

MemoryProvider 接口中新增两个方法:

export interface MemoryProvider {
  // ... 已有方法保持不变
  page(opts: MemoryPageOpts): Promise<MemoryPageResult>;
  count(opts: Omit<MemoryPageOpts, 'page' | 'size'>): Promise<number>;
}
  • Step 2: 提交
git add packages/backend/src/modules/netaclaw/memory/provider.ts
git commit -m "feat(memory): extend MemoryProvider with page/count and optional userId"

Task 4: MysqlMemoryProvider 实现 page/count

Files:

  • Modify: packages/backend/src/modules/netaclaw/memory/mysql_provider.ts

  • Step 1: 添加 page 方法

MysqlMemoryProvider 类中,getById 方法之后添加:

async page(opts: MemoryPageOpts): Promise<MemoryPageResult> {
  const qb = this.repo.createQueryBuilder('m');
  if (opts.agentName) qb.andWhere('m.agentName = :agentName', { agentName: opts.agentName });
  if (opts.userId) qb.andWhere('m.userId = :userId', { userId: opts.userId });
  if (opts.type) qb.andWhere('m.type = :type', { type: opts.type });
  if (opts.keyWord) {
    qb.andWhere('(m.name LIKE :kw OR m.content LIKE :kw OR m.description LIKE :kw)', { kw: `%${opts.keyWord}%` });
  }
  qb.orderBy('m.updateTime', 'DESC');
  const total = await qb.getCount();
  const list = await qb.skip((opts.page - 1) * opts.size).take(opts.size).getMany();
  return { list: list.map(toEntry), pagination: { page: opts.page, size: opts.size, total } };
}

async count(opts: Omit<MemoryPageOpts, 'page' | 'size'>): Promise<number> {
  const qb = this.repo.createQueryBuilder('m');
  if (opts.agentName) qb.andWhere('m.agentName = :agentName', { agentName: opts.agentName });
  if (opts.userId) qb.andWhere('m.userId = :userId', { userId: opts.userId });
  if (opts.type) qb.andWhere('m.type = :type', { type: opts.type });
  return qb.getCount();
}

同时在文件顶部添加 import

import { MemoryProvider, MemoryEntry, MemorySearchOpts, MemoryPageOpts, MemoryPageResult } from './provider.js';
  • Step 2: 修复 search/list 中 userId 可选

searchlist 方法中,将 userId 条件改为可选:

// search 方法中
if (opts.userId) {
  qb = qb.andWhere('m.userId = :userId', { userId: opts.userId });
}

// list 方法中
const where: any = { agentName: opts.agentName };
if (opts.userId) where.userId = opts.userId;
if (opts.type) where.type = opts.type;
  • Step 3: 提交
git add packages/backend/src/modules/netaclaw/memory/mysql_provider.ts
git commit -m "feat(memory): implement page/count in MysqlMemoryProvider"

Task 5: SqliteMemoryProvider — CHECK 迁移 + page/count + busy_timeout

Files:

  • Modify: packages/backend/src/modules/netaclaw/memory/sqlite_provider.ts

  • Step 1: 修改 INIT_SQL 去掉 CHECK 约束

将现有的 INIT_SQL 中:

type TEXT NOT NULL CHECK(type IN ('user', 'project', 'feedback', 'reference')),

改为:

type TEXT NOT NULL,
  • Step 2: 添加自动迁移逻辑

在构造函数中,this.db.exec(INIT_SQL) 之前添加迁移逻辑:

this.migrateCheckConstraint();

在类中添加迁移方法:

private migrateCheckConstraint(): void {
  const row = this.db.prepare(
    "SELECT sql FROM sqlite_master WHERE type='table' AND name='memory'"
  ).get() as { sql: string } | undefined;
  if (!row || !row.sql.includes('CHECK')) return;

  this.db.exec(`
    CREATE TABLE memory_new (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      agent_name TEXT NOT NULL,
      user_id TEXT NOT NULL,
      type TEXT NOT NULL,
      name TEXT NOT NULL,
      content TEXT NOT NULL,
      description TEXT NOT NULL DEFAULT '',
      metadata TEXT,
      created_at TEXT DEFAULT (datetime('now')),
      updated_at TEXT DEFAULT (datetime('now'))
    );
    INSERT INTO memory_new SELECT * FROM memory;
    DROP TABLE memory;
    ALTER TABLE memory_new RENAME TO memory;
    CREATE INDEX IF NOT EXISTS idx_agent_user ON memory(agent_name, user_id);
  `);
  // FTS 和触发器会在后续的 exec(FTS_SQL) 和 exec(TRIGGER_SQL) 中重建
  this.db.exec('DROP TABLE IF EXISTS memory_fts');
}
  • Step 3: 添加 busy_timeout

在构造函数中 this.db.pragma('journal_mode = WAL') 之后添加:

this.db.pragma('busy_timeout = 5000');
  • Step 4: 添加 page/count 方法

在类中添加:

async page(opts: MemoryPageOpts): Promise<MemoryPageResult> {
  let countSql = 'SELECT COUNT(*) as total FROM memory WHERE 1=1';
  let dataSql = 'SELECT * FROM memory WHERE 1=1';
  const params: any[] = [];
  const countParams: any[] = [];

  if (opts.agentName) {
    countSql += ' AND agent_name = ?'; dataSql += ' AND agent_name = ?';
    params.push(opts.agentName); countParams.push(opts.agentName);
  }
  if (opts.userId) {
    countSql += ' AND user_id = ?'; dataSql += ' AND user_id = ?';
    params.push(opts.userId); countParams.push(opts.userId);
  }
  if (opts.type) {
    countSql += ' AND type = ?'; dataSql += ' AND type = ?';
    params.push(opts.type); countParams.push(opts.type);
  }
  if (opts.keyWord) {
    const kw = `%${opts.keyWord}%`;
    countSql += ' AND (name LIKE ? OR content LIKE ? OR description LIKE ?)';
    dataSql += ' AND (name LIKE ? OR content LIKE ? OR description LIKE ?)';
    params.push(kw, kw, kw); countParams.push(kw, kw, kw);
  }

  const { total } = this.db.prepare(countSql).get(...countParams) as { total: number };
  dataSql += ' ORDER BY updated_at DESC LIMIT ? OFFSET ?';
  params.push(opts.size, (opts.page - 1) * opts.size);
  const rows = this.db.prepare(dataSql).all(...params);
  return { list: rows.map(toEntry), pagination: { page: opts.page, size: opts.size, total } };
}

async count(opts: Omit<MemoryPageOpts, 'page' | 'size'>): Promise<number> {
  let sql = 'SELECT COUNT(*) as total FROM memory WHERE 1=1';
  const params: any[] = [];
  if (opts.agentName) { sql += ' AND agent_name = ?'; params.push(opts.agentName); }
  if (opts.userId) { sql += ' AND user_id = ?'; params.push(opts.userId); }
  if (opts.type) { sql += ' AND type = ?'; params.push(opts.type); }
  const { total } = this.db.prepare(sql).get(...params) as { total: number };
  return total;
}

同时在文件顶部添加 import

import { MemoryProvider, MemoryEntry, MemorySearchOpts, MemoryPageOpts, MemoryPageResult } from './provider.js';
  • Step 5: 修复 search/list 中 userId 可选

search 方法中,将 AND m.user_id = ? 改为条件判断:

if (opts.userId) { sql += ' AND m.user_id = ?'; params.push(opts.userId); }

list 方法中同理:

if (opts.userId) { sql += ' AND user_id = ?'; params.push(opts.userId); }
  • Step 6: 提交
git add packages/backend/src/modules/netaclaw/memory/sqlite_provider.ts
git commit -m "feat(memory): sqlite CHECK migration, page/count, busy_timeout"

Task 6: MemoryProviderRegistry 缓存池

Files:

  • Create: packages/backend/src/modules/netaclaw/memory/registry.ts

  • Step 1: 创建 Registry

// packages/backend/src/modules/netaclaw/memory/registry.ts
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawAgentEntity } from '../entity/agent.js';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { MemoryProvider, AgentMemoryConfig } from './provider.js';
import { createMemoryProvider } from './factory.js';

interface CachedProvider {
  provider: MemoryProvider;
  backend: string;
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class MemoryProviderRegistry {
  private cache = new Map<string, CachedProvider>();

  @InjectEntityModel(NetaClawAgentEntity)
  agentEntity: Repository<NetaClawAgentEntity>;

  @InjectEntityModel(NetaClawMemoryEntity)
  memoryEntity: Repository<NetaClawMemoryEntity>;

  async getProvider(agentName: string): Promise<CachedProvider> {
    const cached = this.cache.get(agentName);
    if (cached) return cached;

    const agent = await this.agentEntity.findOneBy({ name: agentName });
    const memoryConfig: AgentMemoryConfig = agent?.config?.memory ?? { enabled: true, backend: 'mysql' };
    const provider = createMemoryProvider(memoryConfig, this.memoryEntity);
    const entry: CachedProvider = { provider, backend: memoryConfig.backend };
    this.cache.set(agentName, entry);
    return entry;
  }

  invalidate(agentName: string): void {
    const cached = this.cache.get(agentName);
    if (cached) {
      cached.provider.close?.();
      this.cache.delete(agentName);
    }
  }

  async getAllAgentsWithMemory(): Promise<Array<{ name: string; backend: string }>> {
    const agents = await this.agentEntity.find();
    return agents
      .filter(a => a.config?.memory?.enabled !== false)
      .map(a => ({
        name: a.name,
        backend: a.config?.memory?.backend ?? 'mysql',
      }));
  }
}
  • Step 2: 提交
git add packages/backend/src/modules/netaclaw/memory/registry.ts
git commit -m "feat(memory): add MemoryProviderRegistry singleton cache"

Task 7: MemoryAdminService

Files:

  • Create: packages/backend/src/modules/netaclaw/service/memory_admin.ts

  • Step 1: 创建 MemoryAdminService

// packages/backend/src/modules/netaclaw/service/memory_admin.ts
import { Provide, Inject } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { MemoryProviderRegistry } from '../memory/registry.js';
import { MemoryEntry } from '../memory/provider.js';

@Provide()
export class MemoryAdminService extends BaseService {
  @Inject()
  registry: MemoryProviderRegistry;

  @InjectEntityModel(NetaClawMemoryEntity)
  memoryEntity: Repository<NetaClawMemoryEntity>;

  async page(params: { agentName?: string; type?: string; keyWord?: string; page: number; size: number }) {
    if (params.agentName) {
      const { provider, backend } = await this.registry.getProvider(params.agentName);
      const result = await provider.page(params);
      result.list.forEach(e => (e as any)._backend = backend);
      return result;
    }

    // 未指定 AgentMySQL 一次查询 + SQLite 逐个查询,合并
    const allAgents = await this.registry.getAllAgentsWithMemory();
    const sqliteAgents = allAgents.filter(a => a.backend === 'sqlite');

    // MySQL 后端:直接查表
    const mysqlResult = await this.pageMysqlDirect(params);
    mysqlResult.list.forEach(e => (e as any)._backend = 'mysql');

    // SQLite 后端
    const sqliteEntries: (MemoryEntry & { _backend: string })[] = [];
    for (const agent of sqliteAgents) {
      const { provider } = await this.registry.getProvider(agent.name);
      const entries = await provider.list({ agentName: agent.name, limit: 1000 });
      entries.forEach(e => sqliteEntries.push({ ...e, _backend: 'sqlite' } as any));
    }

    // 合并排序分页
    const all = [...mysqlResult.list, ...sqliteEntries]
      .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
    const total = mysqlResult.pagination.total + sqliteEntries.length;
    const start = (params.page - 1) * params.size;
    const list = all.slice(start, start + params.size);
    return { list, pagination: { page: params.page, size: params.size, total } };
  }

  private async pageMysqlDirect(params: { type?: string; keyWord?: string; page: number; size: number }) {
    const qb = this.memoryEntity.createQueryBuilder('m');
    if (params.type) qb.andWhere('m.type = :type', { type: params.type });
    if (params.keyWord) {
      qb.andWhere('(m.name LIKE :kw OR m.content LIKE :kw OR m.description LIKE :kw)', { kw: `%${params.keyWord}%` });
    }
    qb.orderBy('m.updateTime', 'DESC');
    const total = await qb.getCount();
    const list = await qb.skip((params.page - 1) * params.size).take(params.size).getMany();
    return {
      list: list.map(e => ({
        id: e.id, agentName: e.agentName, userId: e.userId, type: e.type,
        name: e.name, content: e.content, description: e.description,
        metadata: e.metadata, createdAt: e.createTime, updatedAt: e.updateTime,
      })),
      pagination: { page: params.page, size: params.size, total },
    };
  }

  async info(agentName: string, id: number) {
    const { provider, backend } = await this.registry.getProvider(agentName);
    const entry = await provider.getById(id);
    if (entry) (entry as any)._backend = backend;
    return entry;
  }

  async add(params: { agentName: string; userId?: string; type: string; name: string; content: string; description?: string; metadata?: Record<string, unknown> }) {
    const { provider } = await this.registry.getProvider(params.agentName);
    return provider.save({
      agentName: params.agentName,
      userId: params.userId ?? 'admin',
      type: params.type,
      name: params.name,
      content: params.content,
      description: params.description ?? '',
      metadata: params.metadata,
    });
  }

  async update(params: { agentName: string; id: number; updatedAt?: string; name?: string; content?: string; description?: string; type?: string; metadata?: Record<string, unknown> }) {
    const { provider } = await this.registry.getProvider(params.agentName);
    if (params.updatedAt) {
      const existing = await provider.getById(params.id);
      // 截断到秒级比较,兼容 MySQL毫秒精度和 SQLite秒级字符串
      const existingTime = new Date(existing.updatedAt).toISOString().slice(0, 19);
      const paramTime = new Date(params.updatedAt).toISOString().slice(0, 19);
      if (existing && existingTime !== paramTime) {
        throw new Error('记忆已被其他操作修改,请刷新后重试');
      }
    }
    return provider.update(params.id, {
      name: params.name,
      content: params.content,
      description: params.description,
      type: params.type,
      metadata: params.metadata,
    });
  }

  async deleteMemories(agentName: string, ids: number[]) {
    const { provider } = await this.registry.getProvider(agentName);
    for (const id of ids) {
      await provider.delete(id);
    }
  }

  async stats(): Promise<Array<{ agentName: string; backend: string; count: number }>> {
    // MySQL: GROUP BY
    const mysqlStats = await this.memoryEntity
      .createQueryBuilder('m')
      .select('m.agentName', 'agentName')
      .addSelect('COUNT(*)', 'count')
      .groupBy('m.agentName')
      .getRawMany();

    // SQLite: 逐个查
    const allAgents = await this.registry.getAllAgentsWithMemory();
    const sqliteAgents = allAgents.filter(a => a.backend === 'sqlite');
    const sqliteStats: Array<{ agentName: string; backend: string; count: number }> = [];
    for (const agent of sqliteAgents) {
      const { provider } = await this.registry.getProvider(agent.name);
      const count = await provider.count({ agentName: agent.name });
      sqliteStats.push({ agentName: agent.name, backend: 'sqlite', count });
    }

    return [
      ...mysqlStats.map((s: any) => ({ agentName: s.agentName, backend: 'mysql', count: Number(s.count) })),
      ...sqliteStats,
    ];
  }
}
  • Step 2: 提交
git add packages/backend/src/modules/netaclaw/service/memory_admin.ts
git commit -m "feat(memory): add MemoryAdminService with dual-backend support"

Task 8: Memory Admin Controller

Files:

  • Create: packages/backend/src/modules/netaclaw/controller/admin/memory.ts

  • Step 1: 创建 Memory Admin Controller

// packages/backend/src/modules/netaclaw/controller/admin/memory.ts
import { Provide, Inject, Get, Post, Body, Query } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { MemoryAdminService } from '../../service/memory_admin.js';

@Provide()
@CoolController()
export class AdminNetaClawMemoryController extends BaseController {
  @Inject()
  memoryAdminService: MemoryAdminService;

  @Post('/page', { summary: '记忆分页查询' })
  async page(@Body() body: { agentName?: string; type?: string; keyWord?: string; page: number; size: number }) {
    const result = await this.memoryAdminService.page(body);
    return this.ok(result);
  }

  @Get('/info', { summary: '记忆详情' })
  async info(@Query('agentName') agentName: string, @Query('id') id: number) {
    if (!agentName || !id) return this.fail('agentName 和 id 必填');
    const entry = await this.memoryAdminService.info(agentName, id);
    if (!entry) return this.fail('记忆不存在');
    return this.ok(entry);
  }

  @Post('/add', { summary: '新增记忆' })
  async add(@Body() body: { agentName: string; userId?: string; type: string; name: string; content: string; description?: string; metadata?: Record<string, unknown> }) {
    if (!body.agentName || !body.type || !body.name) return this.fail('agentName、type、name 必填');
    const entry = await this.memoryAdminService.add(body);
    return this.ok(entry);
  }

  @Post('/update', { summary: '更新记忆(乐观锁)' })
  async update(@Body() body: { agentName: string; id: number; updatedAt?: string; name?: string; content?: string; description?: string; type?: string; metadata?: Record<string, unknown> }) {
    if (!body.agentName || !body.id) return this.fail('agentName 和 id 必填');
    try {
      const entry = await this.memoryAdminService.update(body);
      return this.ok(entry);
    } catch (e: any) {
      return this.fail(e.message);
    }
  }

  @Post('/delete', { summary: '删除记忆' })
  async delete(@Body() body: { agentName: string; ids: number[] }) {
    if (!body.agentName || !body.ids?.length) return this.fail('agentName 和 ids 必填');
    await this.memoryAdminService.deleteMemories(body.agentName, body.ids);
    return this.ok();
  }

  @Get('/stats', { summary: '记忆统计' })
  async stats() {
    const result = await this.memoryAdminService.stats();
    return this.ok(result);
  }
}
  • Step 2: 验证接口

启动后端,测试 stats 接口:

curl http://127.0.0.1:8003/admin/netaclaw/memory/stats -H "Authorization: Bearer <token>"
  • Step 3: 提交
git add packages/backend/src/modules/netaclaw/controller/admin/memory.ts
git commit -m "feat(memory): add Memory admin controller with dual-backend routing"

Task 9: Agent 工具扩展 — memory_save/recall + 新工具

Files:

  • Modify: packages/backend/src/modules/netaclaw/tools/builtin/memory.ts

  • Create: packages/backend/src/modules/netaclaw/tools/builtin/memory_types.ts

  • Modify: packages/backend/src/modules/netaclaw/tools/catalog.ts

  • Modify: packages/backend/src/modules/netaclaw/service/tool_resolver.ts:574-584

  • Step 1: 扩展 memory_save — 支持 metadata + 动态类型

packages/backend/src/modules/netaclaw/tools/builtin/memory.ts 中:

MemoryTypeSchema 从固定枚举改为:

const MemoryTypeSchema = Type.String({ description: '记忆类型(如 user, project, feedback, reference 或自定义类型)' });

memorySaveParams 中添加 metadata

const memorySaveParams = Type.Object({
  action: Type.Union([Type.Literal('create'), Type.Literal('update'), Type.Literal('delete')]),
  name: Type.String({ description: '记忆标题' }),
  type: MemoryTypeSchema,
  content: Type.Optional(Type.String({ description: '记忆正文' })),
  description: Type.Optional(Type.String({ description: '一行描述' })),
  id: Type.Optional(Type.Number({ description: '更新/删除时的记忆 ID' })),
  metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: '结构化元数据' })),
});

createMemorySaveTool 的函数签名中新增 memoryTypeRepo 参数:

export function createMemorySaveTool(
  provider: MemoryProvider,
  agentName: string,
  userId: string,
  memoryTypeRepo?: Repository<NetaClawMemoryTypeEntity>,
): AnyAgentTool {

execute 方法开头添加类型校验 Fallback

async execute(_id: string, params: MemorySaveParams): Promise<string> {
  // 类型校验 Fallback优先查 DBMySQL 不可用时回退内置类型
  const BUILTIN = ['user', 'project', 'feedback', 'reference'];
  let validTypes = BUILTIN;
  if (memoryTypeRepo) {
    try {
      const dbTypes = await memoryTypeRepo.find();
      if (dbTypes.length > 0) validTypes = dbTypes.map(t => t.key);
    } catch { /* fallback */ }
  }
  if (!validTypes.includes(params.type)) {
    return `警告: 类型 "${params.type}" 不在已注册类型中 (${validTypes.join(', ')})。请使用 memory_list_types 查看可用类型。`;
  }

execute 中,create 分支添加 metadata

if (params.action === 'create') {
  const entry = await provider.save({
    agentName, userId,
    type: params.type as MemoryType,
    name: params.name,
    content: params.content ?? '',
    description: params.description ?? '',
    metadata: params.metadata,
  });
  return `记忆已保存 (id=${entry.id}): ${entry.name}`;
}

update 分支也添加 metadata

if (params.action === 'update') {
  if (!params.id) return '错误: 更新操作需要提供 id';
  const entry = await provider.update(params.id, {
    name: params.name,
    type: params.type as MemoryType,
    content: params.content,
    description: params.description,
    metadata: params.metadata,
  });
  return `记忆已更新 (id=${entry.id}): ${entry.name}`;
}
  • Step 2: 扩展 memory_recall — 支持 id 查询 + metadata 输出

memoryRecallParams 中添加 id

const memoryRecallParams = Type.Object({
  query: Type.Optional(Type.String({ description: '搜索关键词,留空则列出所有记忆' })),
  type: Type.Optional(MemoryTypeSchema),
  limit: Type.Optional(Type.Number({ description: '返回条数,默认 5', default: 5 })),
  id: Type.Optional(Type.Number({ description: '按 ID 精确查询' })),
});

execute 方法开头添加 id 查询分支:

async execute(_id: string, params: MemoryRecallParams): Promise<string> {
  if (params.id) {
    const entry = await provider.getById(params.id);
    if (!entry) return '未找到该记忆。';
    const meta = entry.metadata ? `\n元数据: ${JSON.stringify(entry.metadata)}` : '';
    return `[${entry.type}] (id=${entry.id}) ${entry.name}\n${entry.content}${meta}`;
  }
  // ... 原有逻辑

修改返回格式,包含 metadata

return entries.map(e => {
  const meta = e.metadata ? `\n元数据: ${JSON.stringify(e.metadata)}` : '';
  return `[${e.type}] (id=${e.id}) ${e.name}\n${e.content}${meta}`;
}).join('\n\n');
  • Step 3: 创建 memory_list_types + memory_stats 工具
// packages/backend/src/modules/netaclaw/tools/builtin/memory_types.ts
import { Type, Static } from '@sinclair/typebox';
import { AnyAgentTool } from '../common.js';
import { MemoryProvider } from '../../memory/provider.js';
import { Repository } from 'typeorm';
import { NetaClawMemoryTypeEntity } from '../../entity/memory_type.js';

const BUILTIN_TYPES = [
  { key: 'user', name: '用户画像', description: '用户偏好、角色、习惯' },
  { key: 'project', name: '项目知识', description: '项目进展、决策、约束' },
  { key: 'feedback', name: '行为反馈', description: '用户对 Agent 行为的纠正或确认' },
  { key: 'reference', name: '引用资源', description: '外部资源链接、文档地址' },
];

export function createMemoryListTypesTool(
  memoryTypeRepo?: Repository<NetaClawMemoryTypeEntity>,
): AnyAgentTool {
  return {
    name: 'memory_list_types',
    label: '查看记忆类型',
    description: '列出所有可用的记忆类型,帮助选择正确的类型存储记忆。',
    parameters: Type.Object({}),
    async execute(): Promise<string> {
      let types = BUILTIN_TYPES;
      if (memoryTypeRepo) {
        try {
          const dbTypes = await memoryTypeRepo.find();
          if (dbTypes.length > 0) {
            types = dbTypes.map(t => ({ key: t.key, name: t.name, description: t.description }));
          }
        } catch { /* fallback to builtin */ }
      }
      return types.map(t => `- ${t.key}: ${t.name}${t.description}`).join('\n');
    },
  };
}

export function createMemoryStatsTool(
  provider: MemoryProvider,
  agentName: string,
  userId: string,
): AnyAgentTool {
  return {
    name: 'memory_stats',
    label: '记忆统计',
    description: '查看已存储的记忆数量和类型分布,帮助决定是更新已有记忆还是创建新的。',
    parameters: Type.Object({}),
    async execute(): Promise<string> {
      const entries = await provider.list({ agentName, userId, limit: 1000 });
      const byType: Record<string, number> = {};
      for (const e of entries) {
        byType[e.type] = (byType[e.type] || 0) + 1;
      }
      const lines = Object.entries(byType).map(([t, c]) => `  ${t}: ${c} 条`);
      return `记忆总数: ${entries.length}\n类型分布:\n${lines.join('\n')}`;
    },
  };
}

import { registerSchema } from '../catalog.js';
registerSchema({
  name: 'memory_list_types',
  toolset: 'memory',
  description: '列出所有可用的记忆类型。',
  capability: 'text',
  visibility: 'tool',
});
registerSchema({
  name: 'memory_stats',
  toolset: 'memory',
  description: '查看记忆数量和类型分布。',
  capability: 'text',
  visibility: 'tool',
});
  • Step 4: 在 catalog.ts 中注册新工具文件

packages/backend/src/modules/netaclaw/tools/catalog.ts 的 import 列表中,import './builtin/memory.js'; 之后添加:

import './builtin/memory_types.js';
  • Step 5: 在 tool_resolver.ts 中注入新工具

packages/backend/src/modules/netaclaw/service/tool_resolver.ts 第 574-584 行的 memory 工具注入块中,扩展为:

if (params.memoryEnabled && filteredNames.includes('memory_save') && filteredNames.includes('memory_recall')) {
  const runtimeConfig: NetaClawAgentRuntimeConfig | undefined = params.agent?.config || undefined;
  const memoryConfig: AgentMemoryConfig | undefined = runtimeConfig?.memory;
  if (memoryConfig?.enabled) {
    const provider = createMemoryProvider(memoryConfig, this.memoryRepo);
    const userId = params.userId ?? 'anonymous';
    const agentName = params.agent?.name ?? 'default';
    runtimeTools.push(createMemorySaveTool(provider, agentName, userId));
    runtimeTools.push(createMemoryRecallTool(provider, agentName, userId));
    if (filteredNames.includes('memory_list_types')) {
      runtimeTools.push(createMemoryListTypesTool(this.memoryTypeRepo));
    }
    if (filteredNames.includes('memory_stats')) {
      runtimeTools.push(createMemoryStatsTool(provider, agentName, userId));
    }
  }
}

需要在 tool_resolver.ts 中添加 import 和注入:

import { createMemoryListTypesTool, createMemoryStatsTool } from '../tools/builtin/memory_types.js';
import { NetaClawMemoryTypeEntity } from '../entity/memory_type.js';
import { MemoryProviderRegistry } from '../memory/registry.js';

// 在类中添加注入
@InjectEntityModel(NetaClawMemoryTypeEntity)
memoryTypeRepo: Repository<NetaClawMemoryTypeEntity>;

@Inject()
memoryProviderRegistry: MemoryProviderRegistry;

同时替换第 574-584 行原有的 memory 工具注入逻辑(原来直接调 createMemoryProvider()),改为通过 Registry 获取缓存的 Provider避免重复创建 SQLite 连接导致 WAL 锁冲突:

if (params.memoryEnabled && filteredNames.includes('memory_save') && filteredNames.includes('memory_recall')) {
  const runtimeConfig: NetaClawAgentRuntimeConfig | undefined = params.agent?.config || undefined;
  const memoryConfig: AgentMemoryConfig | undefined = runtimeConfig?.memory;
  if (memoryConfig?.enabled) {
    const agentName = params.agent?.name ?? 'default';
    const userId = params.userId ?? 'anonymous';
    const { provider } = await this.memoryProviderRegistry.getProvider(agentName);
    runtimeTools.push(createMemorySaveTool(provider, agentName, userId, this.memoryTypeRepo));
    runtimeTools.push(createMemoryRecallTool(provider, agentName, userId));
    if (filteredNames.includes('memory_list_types')) {
      runtimeTools.push(createMemoryListTypesTool(this.memoryTypeRepo));
    }
    if (filteredNames.includes('memory_stats')) {
      runtimeTools.push(createMemoryStatsTool(provider, agentName, userId));
    }
  }
}
  • Step 6: 提交
git add packages/backend/src/modules/netaclaw/tools/builtin/memory.ts packages/backend/src/modules/netaclaw/tools/builtin/memory_types.ts packages/backend/src/modules/netaclaw/tools/catalog.ts packages/backend/src/modules/netaclaw/service/tool_resolver.ts
git commit -m "feat(memory): extend agent tools with metadata, dynamic types, list_types, stats"

Task 10: 前端记忆管理页面

Files:

  • Create: packages/frontend/src/modules/agent/views/memory.vue

  • Modify: packages/frontend/src/modules/agent/config.ts

  • Step 1: 注册路由

packages/frontend/src/modules/agent/config.tsviews 数组中,crew-monitor 之后添加:

{
  path: '/agent/memory',
  meta: { label: '记忆管理' },
  component: () => import('./views/memory.vue')
}
  • Step 2: 创建记忆管理页面

创建 packages/frontend/src/modules/agent/views/memory.vue,包含:

  • 左侧统计卡片(调 stats 接口)
  • 右侧表格(分页、筛选、增删改查)
  • 类型管理弹窗
  • 新增/编辑记忆弹窗

页面使用 Vue 3 Composition API + Element Plus 组件,通过 useCool()service.request() 调用自定义后端接口。类型管理部分通过 service.netaclaw.memory_type 代理调用标准 CRUD。

关键 API 调用模式:

const { service } = useCool();

// 记忆管理(自定义接口)
service.request({ url: '/admin/netaclaw/memory/page', method: 'POST', data: { page: 1, size: 20 } })
service.request({ url: '/admin/netaclaw/memory/stats' })
service.request({ url: '/admin/netaclaw/memory/add', method: 'POST', data: {...} })
service.request({ url: '/admin/netaclaw/memory/update', method: 'POST', data: {...} })
service.request({ url: '/admin/netaclaw/memory/delete', method: 'POST', data: {...} })

// 类型管理(标准 CRUD 代理)
service.netaclaw.memory_type.list()
service.netaclaw.memory_type.add({...})
service.netaclaw.memory_type.delete({ ids: [...] })

页面布局要点:

  • el-container 左右布局,左侧 240px 统计面板
  • 统计卡片用 el-card,显示 Agent 名称 + 存储后端标签 + 记忆数量
  • 表格列ID、标题、类型el-tag 彩色、Agent、存储后端el-tag、描述、更新时间、操作
  • 筛选栏Agent 下拉 + 类型下拉 + 关键词输入 + 搜索按钮
  • 新增/编辑弹窗el-dialog + el-form
  • 类型管理弹窗el-dialog + el-table系统类型行禁用删除按钮

(此步骤代码量较大,实施时根据项目现有前端模式编写完整 Vue 组件)

  • Step 3: 提交
git add packages/frontend/src/modules/agent/views/memory.vue packages/frontend/src/modules/agent/config.ts
git commit -m "feat(memory): add memory management frontend page"

Task 11: 数据库菜单配置

  • Step 1: 插入菜单记录

通过 MCP MySQL 工具执行:

INSERT INTO base_sys_menu (parentId, name, router, viewPath, orderNum, type, isShow, createTime, updateTime)
VALUES (112, '记忆管理', '/agent/memory', 'modules/agent/views/memory.vue', 8, 1, 1, NOW(), NOW());
  • Step 2: 验证菜单显示

启动前后端,登录管理后台,确认左侧菜单 Agent 管理下出现"记忆管理"菜单项,点击可正常加载页面。

  • Step 3: 提交(无代码变更,仅数据库)

无需 git 提交,菜单数据在数据库中。


Task 12: 端到端验证

  • Step 1: 验证类型管理
  1. 打开记忆管理页面 → 点击"类型管理"
  2. 确认 4 个系统类型显示且不可删除
  3. 新增自定义类型(如 key=environment, name=环境信息)
  4. 确认新类型出现在列表中
  • Step 2: 验证记忆 CRUD
  1. 点击"新增记忆" → 选择 Agent → 选择类型 → 填写内容 → 保存
  2. 确认表格中出现新记忆,存储后端标签正确
  3. 编辑记忆 → 修改内容 → 保存
  4. 删除记忆 → 确认消失
  • Step 3: 验证统计和筛选
  1. 左侧统计卡片显示各 Agent 记忆数量
  2. 点击卡片 → 表格自动筛选
  3. 按类型筛选 → 结果正确
  4. 关键词搜索 → 结果正确
  • Step 4: 验证 Agent 工具
  1. 在 Agent 对话中测试 memory_save 存储自定义类型记忆
  2. 测试 memory_recall 返回包含 metadata 的结果
  3. 测试 memory_list_types 返回包含自定义类型的列表
  4. 测试 memory_stats 返回正确的统计信息
  • Step 5: 最终提交
git add -A
git commit -m "feat(memory): complete memory management module with admin UI"