GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-26-memory-management.md

1157 lines
40 KiB
Markdown
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
# 记忆管理模块实施计划
> **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<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**
```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 <token>" -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<MemoryPageResult>;
count(opts: Omit<MemoryPageOpts, 'page' | 'size'>): Promise<number>;
}
```
- [ ] **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<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
```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<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
```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<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: 提交**
```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<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: 提交**
```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<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 接口:
```bash
curl http://127.0.0.1:8003/admin/netaclaw/memory/stats -H "Authorization: Bearer <token>"
```
- [ ] **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<NetaClawMemoryTypeEntity>,
): AnyAgentTool {
```
`execute` 方法开头添加类型校验 Fallback
```typescript
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
```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<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
```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<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';` 之后添加:
```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<NetaClawMemoryTypeEntity>;
@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"
```