# 记忆管理模块实施计划 > **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** ```typescript // 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): ```typescript import * as entity45 from './modules/netaclaw/entity/memory_type'; ``` 在 `entities` 数组末尾添加(编号与 import 一致): ```typescript ...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 工具执行: ```sql 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: 提交** ```bash 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** ```typescript // 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; async deleteWithCheck(ids: number[]): Promise { 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 { return this.memoryTypeEntity.find({ order: { isSystem: 'DESC', createTime: 'ASC' } }); } } ``` - [ ] **Step 2: 创建 MemoryType Controller** ```typescript // 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 或浏览器测试: ```bash curl -X POST http://127.0.0.1:8003/admin/netaclaw/memory_type/list -H "Authorization: Bearer " -H "Content-Type: application/json" ``` 预期返回 4 条系统类型数据。 - [ ] **Step 4: 提交** ```bash 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` 接口之前,添加新的类型定义,并修改 `MemorySearchOpts` 的 `userId` 为可选: **替换**现有的 `MemoryType` 定义(第 5 行)和 `MemorySearchOpts`(第 7-12 行): ```typescript // 替换第 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` 接口中新增两个方法: ```typescript export interface MemoryProvider { // ... 已有方法保持不变 page(opts: MemoryPageOpts): Promise; count(opts: Omit): Promise; } ``` - [ ] **Step 2: 提交** ```bash 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` 方法之后添加: ```typescript async page(opts: MemoryPageOpts): Promise { 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): Promise { 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: ```typescript import { MemoryProvider, MemoryEntry, MemorySearchOpts, MemoryPageOpts, MemoryPageResult } from './provider.js'; ``` - [ ] **Step 2: 修复 search/list 中 userId 可选** 在 `search` 和 `list` 方法中,将 `userId` 条件改为可选: ```typescript // 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: 提交** ```bash 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` 中: ```sql type TEXT NOT NULL CHECK(type IN ('user', 'project', 'feedback', 'reference')), ``` 改为: ```sql type TEXT NOT NULL, ``` - [ ] **Step 2: 添加自动迁移逻辑** 在构造函数中,`this.db.exec(INIT_SQL)` 之前添加迁移逻辑: ```typescript this.migrateCheckConstraint(); ``` 在类中添加迁移方法: ```typescript 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')` 之后添加: ```typescript this.db.pragma('busy_timeout = 5000'); ``` - [ ] **Step 4: 添加 page/count 方法** 在类中添加: ```typescript async page(opts: MemoryPageOpts): Promise { 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): Promise { 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: ```typescript import { MemoryProvider, MemoryEntry, MemorySearchOpts, MemoryPageOpts, MemoryPageResult } from './provider.js'; ``` - [ ] **Step 5: 修复 search/list 中 userId 可选** 在 `search` 方法中,将 `AND m.user_id = ?` 改为条件判断: ```typescript if (opts.userId) { sql += ' AND m.user_id = ?'; params.push(opts.userId); } ``` 在 `list` 方法中同理: ```typescript if (opts.userId) { sql += ' AND user_id = ?'; params.push(opts.userId); } ``` - [ ] **Step 6: 提交** ```bash 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** ```typescript // 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(); @InjectEntityModel(NetaClawAgentEntity) agentEntity: Repository; @InjectEntityModel(NetaClawMemoryEntity) memoryEntity: Repository; async getProvider(agentName: string): Promise { 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> { 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: 提交** ```bash 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** ```typescript // 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; 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; } // 未指定 Agent:MySQL 一次查询 + 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 }) { 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 }) { 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> { // 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: 提交** ```bash 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** ```typescript // 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 }) { 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 }) { 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 接口: ```bash curl http://127.0.0.1:8003/admin/netaclaw/memory/stats -H "Authorization: Bearer " ``` - [ ] **Step 3: 提交** ```bash 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` 从固定枚举改为: ```typescript const MemoryTypeSchema = Type.String({ description: '记忆类型(如 user, project, feedback, reference 或自定义类型)' }); ``` 在 `memorySaveParams` 中添加 metadata: ```typescript 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` 参数: ```typescript export function createMemorySaveTool( provider: MemoryProvider, agentName: string, userId: string, memoryTypeRepo?: Repository, ): AnyAgentTool { ``` 在 `execute` 方法开头添加类型校验 Fallback: ```typescript async execute(_id: string, params: MemorySaveParams): Promise { // 类型校验 Fallback:优先查 DB,MySQL 不可用时回退内置类型 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: ```typescript 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: ```typescript 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: ```typescript 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 查询分支: ```typescript async execute(_id: string, params: MemoryRecallParams): Promise { 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: ```typescript 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 工具** ```typescript // 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, ): AnyAgentTool { return { name: 'memory_list_types', label: '查看记忆类型', description: '列出所有可用的记忆类型,帮助选择正确的类型存储记忆。', parameters: Type.Object({}), async execute(): Promise { 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 { const entries = await provider.list({ agentName, userId, limit: 1000 }); const byType: Record = {}; 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';` 之后添加: ```typescript import './builtin/memory_types.js'; ``` - [ ] **Step 5: 在 tool_resolver.ts 中注入新工具** 在 `packages/backend/src/modules/netaclaw/service/tool_resolver.ts` 第 574-584 行的 memory 工具注入块中,扩展为: ```typescript 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 和注入: ```typescript 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; @Inject() memoryProviderRegistry: MemoryProviderRegistry; ``` 同时**替换**第 574-584 行原有的 memory 工具注入逻辑(原来直接调 `createMemoryProvider()`),改为通过 Registry 获取缓存的 Provider,避免重复创建 SQLite 连接导致 WAL 锁冲突: ```typescript 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: 提交** ```bash 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.ts` 的 `views` 数组中,`crew-monitor` 之后添加: ```typescript { 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 调用模式: ```typescript 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: 提交** ```bash 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 工具执行: ```sql 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: 最终提交** ```bash git add -A git commit -m "feat(memory): complete memory management module with admin UI" ```