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

1157 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 记忆管理模块实施计划
> **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"
```