1157 lines
40 KiB
Markdown
1157 lines
40 KiB
Markdown
|
|
# 记忆管理模块实施计划
|
|||
|
|
|
|||
|
|
> **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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 未指定 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<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:优先查 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<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"
|
|||
|
|
```
|