refactor: 工程标准化 — 组件提取、废弃API清理、console清零

- ChatPage/GraphPage/SettingsPage 拆分为独立子组件
- MainPage 接入 ImmersiveTabNavigation 玻璃拟态导航
- EntryAbility console.* 全替换为 defaultLogger
- SplashPage replaceUrl 废弃 API 迁移到 UIContext Router
- common/Index.ets 双通道导出 Logger/defaultLogger
- 删除 dead code (entry/, commonbusiness/, trulymem-core/)
- 全工程 0 console.*,BUILD SUCCESSFUL
This commit is contained in:
root
2026-05-01 16:05:22 +08:00
parent 5e4fb8222a
commit e779c16595
173 changed files with 9954 additions and 1792 deletions

17
common/BuildProfile.ets Normal file
View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '1.0.0';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
export const TARGET_NAME = 'default';
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly HAR_VERSION = HAR_VERSION;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
static readonly TARGET_NAME = TARGET_NAME;
}

View File

@ -1,7 +1,24 @@
export { TrulyMEMConstants } from './src/main/ets/constant/TrulyMEMConstants';
export { GraphDatabase, RecallEntity, TimeRangeParams } from './src/main/ets/model/GraphDatabase';
export { GraphMemoryService } from './src/main/ets/service/GraphMemoryService';
export { AIAgentService, ChatMessage, AgentResponse } from './src/main/ets/service/AIAgentService';
export { PageContext, RouterParam, IPageContext } from './src/main/ets/routermanager/PageContext';
export { BreakpointType, BreakpointTypes, WidthBreakpoint } from './src/main/ets/util/BreakpointSystem';
export { BaseViewModel, VMEvent } from './src/main/ets/viewmodel/BaseViewModel';
// ========= Utility Layer =========
export { defaultLogger } from './src/main/ets/util/Logger';
export { defaultLogger as Logger } from './src/main/ets/util/Logger';
export { BreakpointType, BreakpointTypes, WidthBreakpoint } from "./src/main/ets/util/BreakpointSystem";
// ========= Router =========
export { PageContext, RouterParam, IPageContext } from "./src/main/ets/routermanager/PageContext";
// ========= Constants =========
export { Constants as TrulyMEMConstants } from "./src/main/ets/constant/TrulyMEMConstants";
// ========= Model Layer =========
export { GraphDatabase, RecallEntity, TimeRangeParams } from "./src/main/ets/model/GraphDatabase";
// ========= Service Layer =========
export { GraphMemoryService, ConnectionItem, NodeDetailInfo } from "./src/main/ets/service/GraphMemoryService";
export { AIAgentService, ChatMessage, AgentResponse } from "./src/main/ets/service/AIAgentService";
// ========= ViewModel Layer =========
export { BaseViewModel, VMEvent } from "./src/main/ets/viewmodel/BaseViewModel";
// ========= Component Layer =========
export { ImmersiveTabNavigation } from "./src/main/ets/component/ImmersiveTabNavigation";

1
common/common Symbolic link
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/common

6
common/hvigorfile.ts Normal file
View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View File

@ -1,3 +1,4 @@
import { defaultLogger } from '../util/Logger';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
@ -19,7 +20,7 @@ export struct ImmersiveTabNavigation {
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
this.bottomAvoidHeight = avoidArea.bottomRect.height || 0;
} catch (e) {
console.error('Failed to get avoid area: ' + (e as BusinessError).message);
defaultLogger.error('Failed to get avoid area: ' + (e as BusinessError).message);
}
}
}

View File

@ -15,6 +15,11 @@ interface CriteriaData {
target?: string;
relation?: string;
sessionId?: string;
sourceType?: string;
targetType?: string;
sourceHasStatus?: string;
subjectContains?: string;
targetContains?: string;
}
interface NodeData {
@ -116,6 +121,20 @@ interface SearchResultItem {
mentions: number;
}
class TaskNodeRow {
id: number = 0;
name: string = '';
updatedAt: string = '';
}
interface DbTaskInfo {
taskId: string;
description: string;
state: string;
infoCount: number;
updatedAt: string;
}
interface ChatMessage {
role: string;
content: string;
@ -529,12 +548,10 @@ export class GraphDatabase {
async purge(criteria: CriteriaData, mode: string = 'soft'): Promise<void> {
if (!this.store) return;
if (!criteria.subject && !criteria.target && !criteria.relation && !criteria.sessionId) {
return;
}
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
let hasCondition = false;
// 精确匹配
if (criteria.subject) {
const subjectId = await this.getNodeIdByName(criteria.subject);
if (subjectId > 0) {
@ -567,6 +584,96 @@ export class GraphDatabase {
hasCondition = true;
}
// 模糊匹配:subjectContains -> 通过子查询匹配节点名
if (criteria.subjectContains) {
const nodeSql = `SELECT id FROM nodes WHERE name LIKE '%' || ? || '%'`;
const nodeResult = await this.store.querySql(nodeSql, [criteria.subjectContains]);
const nodeIds: number[] = [];
while (nodeResult.goToNextRow()) {
nodeIds.push(nodeResult.getLong(0));
}
nodeResult.close();
if (nodeIds.length > 0) {
if (hasCondition) {
predicates.and();
}
predicates.in('subject_id', nodeIds);
hasCondition = true;
}
}
// 模糊匹配:targetContains
if (criteria.targetContains) {
const nodeSql = `SELECT id FROM nodes WHERE name LIKE '%' || ? || '%'`;
const nodeResult = await this.store.querySql(nodeSql, [criteria.targetContains]);
const nodeIds: number[] = [];
while (nodeResult.goToNextRow()) {
nodeIds.push(nodeResult.getLong(0));
}
nodeResult.close();
if (nodeIds.length > 0) {
if (hasCondition) {
predicates.and();
}
predicates.in('object_id', nodeIds);
hasCondition = true;
}
}
// 源实体类型过滤
if (criteria.sourceType) {
const nodeSql = `SELECT id FROM nodes WHERE type = ?`;
const nodeResult = await this.store.querySql(nodeSql, [criteria.sourceType]);
const nodeIds: number[] = [];
while (nodeResult.goToNextRow()) {
nodeIds.push(nodeResult.getLong(0));
}
nodeResult.close();
if (nodeIds.length > 0) {
if (hasCondition) {
predicates.and();
}
predicates.in('subject_id', nodeIds);
hasCondition = true;
}
}
// 目标实体类型过滤
if (criteria.targetType) {
const nodeSql = `SELECT id FROM nodes WHERE type = ?`;
const nodeResult = await this.store.querySql(nodeSql, [criteria.targetType]);
const nodeIds: number[] = [];
while (nodeResult.goToNextRow()) {
nodeIds.push(nodeResult.getLong(0));
}
nodeResult.close();
if (nodeIds.length > 0) {
if (hasCondition) {
predicates.and();
}
predicates.in('object_id', nodeIds);
hasCondition = true;
}
}
// 源实体状态过滤
if (criteria.sourceHasStatus) {
const nodeSql = `SELECT id FROM nodes WHERE type LIKE '%' || ? || '%'`;
const nodeResult = await this.store.querySql(nodeSql, [criteria.sourceHasStatus]);
const nodeIds: number[] = [];
while (nodeResult.goToNextRow()) {
nodeIds.push(nodeResult.getLong(0));
}
nodeResult.close();
if (nodeIds.length > 0) {
if (hasCondition) {
predicates.and();
}
predicates.in('subject_id', nodeIds);
hasCondition = true;
}
}
if (hasCondition) {
if (mode === 'soft') {
const bucket: relationalStore.ValuesBucket = {
@ -687,43 +794,84 @@ export class GraphDatabase {
*/
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'archived');
if (keyword) {
predicates.and().like('relation', '%' + keyword + '%');
}
let sql = `
SELECT r.subject_id, r.object_id, r.relation, r.weight, r.session_id, r.turn_id,
e.name AS source_name, t.name AS target_name
FROM relations r
JOIN nodes e ON r.subject_id = e.id
JOIN nodes t ON r.object_id = t.id
WHERE r.status = 'archived'
`;
const params: string[] = [];
if (days && days > 0) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
const maxDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
predicates.and().lessThanOrEqualTo('date_bucket', maxDateBucket);
const minDateBucket = cutoff.toISOString().slice(0, 10);
sql += ' AND r.date_bucket >= ?';
params.push(minDateBucket);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
if (keyword) {
const kw = '%' + keyword + '%';
sql += ' AND (e.name LIKE ? OR t.name LIKE ? OR r.relation LIKE ?)';
params.push(kw, kw, kw);
}
sql += ' ORDER BY r.updated_at DESC LIMIT 200';
const resultSet: relationalStore.ResultSet = await this.store.querySql(sql, params);
const results: RelationQueryResult[] = [];
while (resultSet.goToNextRow()) {
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
const sourceNode = await this.getNodeById(sourceId);
const targetNode = await this.getNodeById(targetId);
if (sourceNode && targetNode) {
const queryResult: RelationQueryResult = {
sourceId: sourceId,
targetId: targetId,
sourceName: sourceNode.name,
targetName: targetNode.name,
type: resultSet.getString(resultSet.getColumnIndex('relation')),
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
depth: 0
};
results.push(queryResult);
}
results.push({
sourceId: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
targetId: resultSet.getLong(resultSet.getColumnIndex('object_id')),
sourceName: resultSet.getString(resultSet.getColumnIndex('source_name')),
targetName: resultSet.getString(resultSet.getColumnIndex('target_name')),
type: resultSet.getString(resultSet.getColumnIndex('relation')),
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
depth: 0
});
}
resultSet.close();
return results;
}
async getRecentTasks(limit: number = 10, stateFilter?: string): Promise<DbTaskInfo[]> {
if (!this.store) return [];
const nodesSql = `SELECT id, name, updated_at FROM nodes WHERE type = 'TaskNode' ORDER BY updated_at DESC LIMIT ?`;
const nodesRs: relationalStore.ResultSet = await this.store.querySql(nodesSql, [String(limit)]);
const taskNodes: TaskNodeRow[] = [];
while (nodesRs.goToNextRow()) {
const tn: TaskNodeRow = new TaskNodeRow();
tn.id = nodesRs.getLong(nodesRs.getColumnIndex('id'));
tn.name = nodesRs.getString(nodesRs.getColumnIndex('name'));
tn.updatedAt = nodesRs.getString(nodesRs.getColumnIndex('updated_at')) || '';
taskNodes.push(tn);
}
nodesRs.close();
const tasks: DbTaskInfo[] = [];
for (const node of taskNodes) {
const relSql = `SELECT r.relation, e.name AS target_name FROM relations r JOIN nodes e ON r.object_id = e.id WHERE r.subject_id = ? AND r.status = 'active' AND r.relation IN ('description', 'has_state')`;
const relRs: relationalStore.ResultSet = await this.store.querySql(relSql, [String(node.id)]);
let description = '';
let state = '进行中';
while (relRs.goToNextRow()) {
const rt: string = relRs.getString(relRs.getColumnIndex('relation'));
const targetName: string = relRs.getString(relRs.getColumnIndex('target_name'));
if (rt === 'description') description = targetName;
if (rt === 'has_state') state = targetName;
}
relRs.close();
if (stateFilter && state !== stateFilter) continue;
const cntSql = `SELECT COUNT(*) AS cnt FROM relations WHERE subject_id = ? AND relation = 'CONTAINS_INFO' AND status = 'active'`;
const cntRs: relationalStore.ResultSet = await this.store.querySql(cntSql, [String(node.id)]);
let infoCount = 0;
if (cntRs.goToNextRow()) infoCount = cntRs.getLong(cntRs.getColumnIndex('cnt'));
cntRs.close();
tasks.push({ taskId: node.name, description, state, infoCount, updatedAt: node.updatedAt });
}
return tasks;
}
private async removeOrphanNodes(): Promise<number> {
if (!this.store) return 0;
let deleted = 0;

View File

@ -0,0 +1,936 @@
import relationalStore from '@ohos.data.relationalStore';
import { Context } from '@ohos.abilityAccessCtrl';
interface NodeNameCacheItem { name: string; type: string; mentions: number; }
export interface TimeRangeParams { days: number; }
interface TripletData {
subject: string;
relation: string;
object: string;
}
interface CriteriaData {
subject?: string;
target?: string;
relation?: string;
sessionId?: string;
}
interface NodeData {
id: number;
label: string;
type: string;
mentions: number;
depth?: number;
}
interface EdgeData {
from: number;
to: number;
label: string;
weight: number;
depth?: number;
sessionId?: string;
turnId?: number;
}
interface GraphData {
nodes: NodeData[];
edges: EdgeData[];
}
export interface RecallEntity {
name: string;
type: string;
mention_count: number;
depth?: number;
}
interface RecallRelation {
source: string;
target: string;
type: string;
confidence: number;
session_id?: string;
turn_id?: number;
depth?: number;
}
interface RecallResult {
entities: RecallEntity[];
relations: RecallRelation[];
message: string;
}
interface BfsEntity {
id: number;
name: string;
type: string;
mentions: number;
depth: number;
}
export interface RelationQueryResult {
sourceId: number;
targetId: number;
sourceName: string;
targetName: string;
type: string;
confidence: number;
sessionId?: string;
turnId?: number;
depth: number;
}
interface NodeQueryResult {
id: number;
name: string;
type: string;
mentions: number;
depth: number;
}
interface CleanupResult {
cleaned: number;
deleted_relations?: number;
deleted_orphans?: number;
dry_run?: boolean;
message?: string;
}
interface IntrospectResult {
entity_count: number;
relation_count: number;
message: string;
}
interface ArchiveResult {
archived: number;
message: string;
}
interface SearchResultItem {
name: string;
type: string;
mentions: number;
}
interface ChatMessage {
role: string;
content: string;
session_id?: string;
}
interface SnapshotData {
entities: RecallEntity[];
relations: RecallRelation[];
}
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'trulymem.db',
securityLevel: relationalStore.SecurityLevel.S1
};
export class GraphDatabase {
private store?: relationalStore.RdbStore;
private context?: Context;
async init(context: Context): Promise<void> {
this.context = context;
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
await this.createTables();
}
private async createTables(): Promise<void> {
if (!this.store) return;
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT DEFAULT 'concept',
mentions INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
`);
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id INTEGER NOT NULL,
relation TEXT NOT NULL,
object_id INTEGER NOT NULL,
weight REAL DEFAULT 1.0,
session_id TEXT,
turn_id INTEGER,
created_at TEXT,
updated_at TEXT,
status TEXT DEFAULT 'active',
date_bucket TEXT
)
`);
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS chat_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
role TEXT NOT NULL,
content TEXT NOT NULL,
tools TEXT,
created_at TEXT
)
`);
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_name ON nodes(name)`);
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_type ON nodes(type)`);
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_source ON relations(subject_id)`);
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_target ON relations(object_id)`);
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_type ON relations(relation)`);
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_status ON relations(status)`);
}
async commit(triplets: TripletData[], entityTypes?: Record<string, string>, sessionId?: string, turnId?: number): Promise<void> {
if (!this.store) return;
for (const triplet of triplets) {
const subjectId: number = await this.upsertNode(triplet.subject, entityTypes?.[triplet.subject]);
const objectId: number = await this.upsertNode(triplet.object, entityTypes?.[triplet.object]);
const existingId: number = await this.checkDuplicateRelation(subjectId, triplet.relation, objectId);
if (existingId > 0) {
continue;
}
const now = new Date().toISOString();
const dateBucket = now.split('T')[0];
const bucket: relationalStore.ValuesBucket = {
'subject_id': subjectId,
'relation': triplet.relation,
'object_id': objectId,
'session_id': sessionId || null,
'turn_id': turnId || null,
'created_at': now,
'updated_at': now,
'status': 'active',
'date_bucket': dateBucket
};
await this.store.insert('relations', bucket);
}
}
private async checkDuplicateRelation(subjectId: number, relation: string, objectId: number): Promise<number> {
if (!this.store) return -1;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('subject_id', subjectId).and().equalTo('relation', relation).and().equalTo('object_id', objectId).and().equalTo('status', 'active');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
if (resultSet.goToFirstRow()) {
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
resultSet.close();
return id;
}
resultSet.close();
return -1;
}
private async upsertNode(name: string, entityType?: string): Promise<number> {
if (!this.store) return -1;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.equalTo('name', name);
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'mentions']);
const now = new Date().toISOString();
if (resultSet.goToNextRow()) {
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
const mentions: number = resultSet.getLong(resultSet.getColumnIndex('mentions'));
resultSet.close();
const updatePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
updatePredicates.equalTo('id', id);
const bucket: relationalStore.ValuesBucket = {
'mentions': mentions + 1,
'updated_at': now
};
await this.store.update(bucket, updatePredicates);
return id;
}
resultSet.close();
const bucket: relationalStore.ValuesBucket = {
'name': name,
'type': entityType || 'concept',
'mentions': 1,
'created_at': now,
'updated_at': now
};
return await this.store.insert('nodes', bucket);
}
async recall(queryIntent: string, seedEntities?: string[], depth: number = 2, timeRange?: TimeRangeParams, sessionFilter?: string): Promise<RecallResult> {
if (!this.store) {
return { entities: [], relations: [], message: 'Database not initialized' };
}
// 计算时间范围过滤
let minDateBucket: string | undefined;
if (timeRange && timeRange.days && timeRange.days > 0) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - timeRange.days);
minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
}
const keywords = queryIntent.toLowerCase().replace(/,/g, ' ').split(/\s+/).filter(w => w.trim());
const allEntities: BfsEntity[] = [];
const entityIds = new Set<number>();
let seedEntityIds = new Set<number>();
if (keywords.length === 0 && (!seedEntities || seedEntities.length === 0)) {
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.orderByDesc('mentions');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
while (resultSet.goToNextRow() && allEntities.length < 50) {
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
const name = resultSet.getString(resultSet.getColumnIndex('name'));
const type = resultSet.getString(resultSet.getColumnIndex('type'));
const mentions = resultSet.getLong(resultSet.getColumnIndex('mentions'));
entityIds.add(id);
allEntities.push({ id, name, type, mentions, depth: 0 });
}
resultSet.close();
} else {
if (seedEntities && seedEntities.length > 0) {
for (const seedName of seedEntities) {
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.equalTo('name', seedName);
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
while (resultSet.goToNextRow()) {
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
if (!entityIds.has(id)) {
entityIds.add(id);
seedEntityIds.add(id);
allEntities.push({
id,
name: resultSet.getString(resultSet.getColumnIndex('name')),
type: resultSet.getString(resultSet.getColumnIndex('type')),
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
depth: 0
});
}
}
resultSet.close();
}
}
for (const keyword of keywords) {
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.like('name', `%${keyword}%`);
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
while (resultSet.goToNextRow()) {
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
if (!entityIds.has(id)) {
entityIds.add(id);
allEntities.push({
id,
name: resultSet.getString(resultSet.getColumnIndex('name')),
type: resultSet.getString(resultSet.getColumnIndex('type')),
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
depth: 0
});
}
}
resultSet.close();
}
}
const allRelations: RelationQueryResult[] = [];
let currentLayerIds = new Set<number>(entityIds);
const visitedEntityIds = new Set<number>(entityIds);
// 批量预加载所有相关节点名称,减少 N+1 查询
const nodeNameCache = new Map<number, NodeNameCacheItem>();
for (let layer = 0; layer < depth && currentLayerIds.size > 0; layer++) {
const currentIds = Array.from(currentLayerIds);
const relations = await this.getRelationsForNodes(currentIds, sessionFilter, minDateBucket);
const nextLayerIds = new Set<number>();
for (const rel of relations) {
allRelations.push(rel);
if (!visitedEntityIds.has(rel.targetId)) {
nextLayerIds.add(rel.targetId);
}
if (rel.targetId !== rel.sourceId && !visitedEntityIds.has(rel.sourceId)) {
nextLayerIds.add(rel.sourceId);
}
}
for (const newId of nextLayerIds) {
if (!visitedEntityIds.has(newId)) {
visitedEntityIds.add(newId);
// 优先从缓存获取,避免 N+1 查询
const cached = nodeNameCache.get(newId);
if (cached) {
const addedEntity: BfsEntity = {
id: newId,
name: cached.name,
type: cached.type,
mentions: cached.mentions,
depth: layer + 1
};
allEntities.push(addedEntity);
} else {
const nodeData = await this.getNodeById(newId);
if (nodeData) {
const cacheItem: NodeNameCacheItem = { name: nodeData.name, type: nodeData.type, mentions: nodeData.mentions };
nodeNameCache.set(newId, cacheItem);
const addedEntity: BfsEntity = {
id: nodeData.id,
name: nodeData.name,
type: nodeData.type,
mentions: nodeData.mentions,
depth: layer + 1
};
allEntities.push(addedEntity);
}
}
}
}
currentLayerIds = nextLayerIds;
}
const entities: RecallEntity[] = allEntities.map(e => {
const entity: RecallEntity = {
name: e.name,
type: e.type,
mention_count: e.mentions,
depth: e.depth
};
return entity;
});
const relations: RecallRelation[] = allRelations.map(r => {
const rel: RecallRelation = {
source: r.sourceName,
target: r.targetName,
type: r.type,
confidence: r.confidence,
session_id: r.sessionId,
turn_id: r.turnId,
depth: r.depth
};
return rel;
});
return {
entities,
relations,
message: `找到 ${entities.length} 个实体, ${relations.length} 条关系`
};
}
private async getNodeById(id: number): Promise<NodeQueryResult | null> {
if (!this.store) return null;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.equalTo('id', id);
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
if (resultSet.goToNextRow()) {
const node: NodeQueryResult = {
id: resultSet.getLong(resultSet.getColumnIndex('id')),
name: resultSet.getString(resultSet.getColumnIndex('name')),
type: resultSet.getString(resultSet.getColumnIndex('type')),
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
depth: 0
};
resultSet.close();
return node;
}
resultSet.close();
return null;
}
private async getRelationsForNodes(nodeIds: number[], sessionFilter?: string, minDateBucket?: string): Promise<RelationQueryResult[]> {
if (!this.store || nodeIds.length === 0) return [];
const relations: RelationQueryResult[] = [];
// 批量预加载所有节点名称到缓存,避免 N+1 查询
const nodeNameCache = new Map<number, NodeNameCacheItem>();
for (const id of nodeIds) {
const node = await this.getNodeById(id);
if (node) {
const cacheItem: NodeNameCacheItem = { name: node.name, type: node.type, mentions: node.mentions };
nodeNameCache.set(id, cacheItem);
}
}
for (const nodeId of nodeIds) {
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'active').and().equalTo('subject_id', nodeId);
if (sessionFilter) {
predicates.and().equalTo('session_id', sessionFilter);
}
if (minDateBucket) {
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
while (resultSet.goToNextRow()) {
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
if (sourceNode && targetNode) {
relations.push({
sourceId,
targetId,
sourceName: sourceNode.name,
targetName: targetNode.name,
type: resultSet.getString(resultSet.getColumnIndex('relation')),
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
depth: 1
});
}
}
resultSet.close();
const predicates2: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates2.equalTo('status', 'active').and().equalTo('object_id', nodeId);
if (sessionFilter) {
predicates2.and().equalTo('session_id', sessionFilter);
}
if (minDateBucket) {
predicates2.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
}
const resultSet2: relationalStore.ResultSet = await this.store.query(predicates2, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
while (resultSet2.goToNextRow()) {
const sourceId = resultSet2.getLong(resultSet2.getColumnIndex('subject_id'));
const targetId = resultSet2.getLong(resultSet2.getColumnIndex('object_id'));
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
if (sourceNode && targetNode) {
relations.push({
sourceId,
targetId,
sourceName: sourceNode.name,
targetName: targetNode.name,
type: resultSet2.getString(resultSet2.getColumnIndex('relation')),
confidence: resultSet2.getDouble(resultSet2.getColumnIndex('weight')),
sessionId: resultSet2.getString(resultSet2.getColumnIndex('session_id')),
turnId: resultSet2.getLong(resultSet2.getColumnIndex('turn_id')),
depth: 1
});
}
}
resultSet2.close();
}
return relations;
}
async search(keyword: string): Promise<SearchResultItem[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.like('name', `%${keyword}%`);
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['name', 'type', 'mentions']);
const results: SearchResultItem[] = [];
while (resultSet.goToNextRow()) {
results.push({
name: resultSet.getString(resultSet.getColumnIndex('name')),
type: resultSet.getString(resultSet.getColumnIndex('type')),
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
});
}
resultSet.close();
return results;
}
async purge(criteria: CriteriaData, mode: string = 'soft'): Promise<void> {
if (!this.store) return;
if (!criteria.subject && !criteria.target && !criteria.relation && !criteria.sessionId) {
return;
}
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
let hasCondition = false;
if (criteria.subject) {
const subjectId = await this.getNodeIdByName(criteria.subject);
if (subjectId > 0) {
predicates.equalTo('subject_id', subjectId);
hasCondition = true;
}
}
if (criteria.target) {
const targetId = await this.getNodeIdByName(criteria.target);
if (targetId > 0) {
if (hasCondition) {
predicates.and();
}
predicates.equalTo('object_id', targetId);
hasCondition = true;
}
}
if (criteria.relation) {
if (hasCondition) {
predicates.and();
}
predicates.equalTo('relation', criteria.relation);
hasCondition = true;
}
if (criteria.sessionId) {
if (hasCondition) {
predicates.and();
}
predicates.equalTo('session_id', criteria.sessionId);
hasCondition = true;
}
if (hasCondition) {
if (mode === 'soft') {
const bucket: relationalStore.ValuesBucket = {
'status': 'deleted',
'updated_at': new Date().toISOString()
};
await this.store.update(bucket, predicates);
} else {
await this.store.delete(predicates);
}
}
await this.removeOrphanNodes();
}
/**
* 记忆图谱 — 在指定时间范围内查询关系和节点
* 对应 tools.memory_graph
*/
async graph(timeRange: TimeRangeParams, sessionFilter?: string): Promise<GraphData> {
if (!this.store) return { nodes: [], edges: [] };
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'active');
if (sessionFilter) {
predicates.and().equalTo('session_id', sessionFilter);
}
if (timeRange && timeRange.days && timeRange.days > 0) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - timeRange.days);
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
const nodeIds = new Set<number>();
const edges: EdgeData[] = [];
while (resultSet.goToNextRow()) {
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
nodeIds.add(sourceId);
nodeIds.add(targetId);
const edge: EdgeData = {
from: sourceId,
to: targetId,
label: resultSet.getString(resultSet.getColumnIndex('relation')),
weight: resultSet.getDouble(resultSet.getColumnIndex('weight')),
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
};
edges.push(edge);
}
resultSet.close();
const nodes: NodeData[] = [];
for (const id of nodeIds) {
const node = await this.getNodeById(id);
if (node) {
const nodeData: NodeData = { id: node.id, label: node.name, type: node.type, mentions: node.mentions };
nodes.push(nodeData);
}
}
const result: GraphData = { nodes, edges };
return result;
}
/**
* 记忆快照 — 在指定时间范围内查询实体和关系
* 对应 tools.memory_snapshot
*/
async snapshot(timeRange: TimeRangeParams, sessionFilter?: string): Promise<SnapshotData> {
if (!this.store) return { entities: [], relations: [] };
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'active');
if (sessionFilter) {
predicates.and().equalTo('session_id', sessionFilter);
}
if (timeRange && timeRange.days && timeRange.days > 0) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - timeRange.days);
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
const nodeIds = new Set<number>();
const relations: RecallRelation[] = [];
while (resultSet.goToNextRow()) {
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
nodeIds.add(sourceId);
nodeIds.add(targetId);
const sourceNode = await this.getNodeById(sourceId);
const targetNode = await this.getNodeById(targetId);
if (sourceNode && targetNode) {
const rel: RecallRelation = {
source: sourceNode.name,
target: targetNode.name,
type: resultSet.getString(resultSet.getColumnIndex('relation')),
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
session_id: resultSet.getString(resultSet.getColumnIndex('session_id')),
turn_id: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
};
relations.push(rel);
}
}
resultSet.close();
const entities: RecallEntity[] = [];
for (const id of nodeIds) {
const node = await this.getNodeById(id);
if (node) {
const recallEntity: RecallEntity = { name: node.name, type: node.type, mention_count: node.mentions };
entities.push(recallEntity);
}
}
const snapResult: SnapshotData = { entities, relations };
return snapResult;
}
/**
* 查询已归档的记忆
* 对应 tools.memory_query_archived
*/
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'archived');
if (keyword) {
predicates.and().like('relation', '%' + keyword + '%');
}
if (days && days > 0) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
const maxDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
predicates.and().lessThanOrEqualTo('date_bucket', maxDateBucket);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
const results: RelationQueryResult[] = [];
while (resultSet.goToNextRow()) {
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
const sourceNode = await this.getNodeById(sourceId);
const targetNode = await this.getNodeById(targetId);
if (sourceNode && targetNode) {
const queryResult: RelationQueryResult = {
sourceId: sourceId,
targetId: targetId,
sourceName: sourceNode.name,
targetName: targetNode.name,
type: resultSet.getString(resultSet.getColumnIndex('relation')),
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
depth: 0
};
results.push(queryResult);
}
}
resultSet.close();
return results;
}
private async removeOrphanNodes(): Promise<number> {
if (!this.store) return 0;
let deleted = 0;
// 优化:批量查询所有有关系的节点 ID,避免 O(N²) 逐节点检查
const activeRelPred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
activeRelPred.equalTo('status', 'active');
const relResultSet: relationalStore.ResultSet = await this.store.query(activeRelPred, ['subject_id', 'object_id']);
const relatedIds = new Set<number>();
while (relResultSet.goToNextRow()) {
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('subject_id')));
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('object_id')));
}
relResultSet.close();
// 查询所有节点,筛选出不在关系中的孤儿节点
const nodePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
const nodeResultSet: relationalStore.ResultSet = await this.store.query(nodePred, ['id']);
const orphanIds: number[] = [];
while (nodeResultSet.goToNextRow()) {
const nodeId = nodeResultSet.getLong(nodeResultSet.getColumnIndex('id'));
if (!relatedIds.has(nodeId)) {
orphanIds.push(nodeId);
}
}
nodeResultSet.close();
// 批量删除孤儿节点
for (const orphanId of orphanIds) {
const deletePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
deletePred.equalTo('id', orphanId);
await this.store.delete(deletePred);
deleted++;
}
return deleted;
}
private async getNodeIdByName(name: string): Promise<number> {
if (!this.store) return -1;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.equalTo('name', name);
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
if (resultSet.goToNextRow()) {
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
resultSet.close();
return id;
}
resultSet.close();
return -1;
}
async introspect(): Promise<IntrospectResult> {
if (!this.store) return { entity_count: 0, relation_count: 0, message: 'Database not initialized' };
const nodePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
const nodeResultSet = await this.store.query(nodePredicates, ['id']);
let entityCount = 0;
while (nodeResultSet.goToNextRow()) {
entityCount++;
}
nodeResultSet.close();
const relPredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
relPredicates.equalTo('status', 'active');
const relResultSet = await this.store.query(relPredicates, ['id']);
let relationCount = 0;
while (relResultSet.goToNextRow()) {
relationCount++;
}
relResultSet.close();
return {
entity_count: entityCount,
relation_count: relationCount,
message: `数据库包含 ${entityCount} 个实体, ${relationCount} 条关系`
};
}
async archive(days: number): Promise<ArchiveResult> {
if (!this.store) return { archived: 0, message: 'Database not initialized' };
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const cutoffStr = cutoffDate.toISOString();
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'active').and().lessThan('created_at', cutoffStr);
const bucket: relationalStore.ValuesBucket = {
'status': 'archived',
'updated_at': new Date().toISOString()
};
const count = await this.store.update(bucket, predicates);
return {
archived: count,
message: `归档了 ${count} 条关系`
};
}
async cleanup(dryRun: boolean): Promise<CleanupResult> {
if (!this.store) return { cleaned: 0, message: 'Database not initialized' };
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 90);
const cutoffStr = cutoffDate.toISOString();
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
predicates.equalTo('status', 'deleted').and().lessThan('updated_at', cutoffStr);
let deleted = 0;
if (dryRun) {
const resultSet = await this.store.query(predicates, ['id']);
while (resultSet.goToNextRow()) {
deleted++;
}
resultSet.close();
} else {
deleted = await this.store.delete(predicates);
const orphanCount = await this.removeOrphanNodes();
return {
cleaned: deleted + orphanCount,
deleted_relations: deleted,
deleted_orphans: orphanCount,
dry_run: false,
message: `删除了 ${deleted} 条关系, ${orphanCount} 个孤立实体`
} as CleanupResult;
}
return {
cleaned: deleted,
deleted_relations: deleted,
dry_run: true,
message: `将删除 ${deleted} 条关系`
} as CleanupResult;
}
async saveChatMessage(role: string, content: string, tools?: string, sessionId?: string): Promise<void> {
if (!this.store) return;
const bucket: relationalStore.ValuesBucket = {
'session_id': sessionId || null,
'role': role,
'content': content,
'tools': tools || null,
'created_at': new Date().toISOString()
};
await this.store.insert('chat_records', bucket);
}
async getChatHistory(limit?: number, sessionId?: string): Promise<ChatMessage[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
if (sessionId) {
predicates.equalTo('session_id', sessionId);
}
predicates.orderByDesc('created_at');
if (limit) {
predicates.limitAs(limit);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['role', 'content', 'session_id']);
const messages: ChatMessage[] = [];
while (resultSet.goToNextRow()) {
const msg: ChatMessage = {
role: resultSet.getString(resultSet.getColumnIndex('role')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
session_id: resultSet.getString(resultSet.getColumnIndex('session_id'))
};
messages.push(msg);
}
resultSet.close();
return messages.reverse();
}
async clearChatHistory(): Promise<void> {
if (!this.store) return;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
await this.store.delete(predicates);
}
private async getAllNodes(): Promise<NodeData[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
const nodes: NodeData[] = [];
while (resultSet.goToNextRow()) {
const node: NodeData = {
id: resultSet.getLong(resultSet.getColumnIndex('id')),
label: resultSet.getString(resultSet.getColumnIndex('name')),
type: resultSet.getString(resultSet.getColumnIndex('type')),
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
};
nodes.push(node);
}
resultSet.close();
return nodes;
}
private async getAllEdges(): Promise<EdgeData[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'relation', 'object_id', 'weight']);
const edges: EdgeData[] = [];
while (resultSet.goToNextRow()) {
const edge: EdgeData = {
from: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
to: resultSet.getLong(resultSet.getColumnIndex('object_id')),
label: resultSet.getString(resultSet.getColumnIndex('relation')),
weight: resultSet.getDouble(resultSet.getColumnIndex('weight'))
};
edges.push(edge);
}
resultSet.close();
return edges;
}
}

View File

@ -1,3 +1,5 @@
import { defaultLogger } from '../util/Logger';
export interface RouterParam {
routerName: string;
param?: object;
@ -24,7 +26,7 @@ export class PageContext implements IPageContext {
try {
this.pathStack.replacePath({ name: data.routerName, param: data.param }, animated);
} catch (err) {
console.error('Open Page ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
defaultLogger.error('replacePage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
}
}
@ -32,7 +34,7 @@ export class PageContext implements IPageContext {
try {
this.pathStack.pushPath({ name: data.routerName, param: data.param }, animated);
} catch (err) {
console.error('Open Page ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
defaultLogger.error('openPage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
}
}
@ -40,7 +42,7 @@ export class PageContext implements IPageContext {
try {
this.pathStack.pop(animated);
} catch (err) {
console.error('Pop Page failed. ' + err.code + ' ' + err.message);
defaultLogger.error('popPage failed. ' + err.code + ' ' + err.message);
}
}

View File

@ -68,6 +68,9 @@ interface ToolPropertiesDefinition {
dryRun?: ToolParamProperty;
keyword?: ToolParamProperty;
attribute?: ToolParamProperty;
sourceType?: ToolParamProperty;
targetType?: ToolParamProperty;
sourceHasStatus?: ToolParamProperty;
}
interface ToolParamProperty {
@ -218,19 +221,23 @@ function buildSystemPrompt(personaContext: string): string {
## 核心身份
${personaContext || '你是一个帮助用户记录和回忆信息的助手。你的核心能力是基于图数据库的记忆系统。'}
## 记忆系统
你的记忆存储在图数据库中,每个记忆都是「实体 - 关系 - 实体」的三元组形式。
每轮对话开始,你都会按以下顺序操作:
## ⚠️ 内部执行顺序(不得向用户输出)
### 步骤1:查询人设
使用 memory_recall 查询当前人设,确保回复风格一致。
以下步骤是内部流程,绝对不要在你的回复中提及或输出。应当仅通过工具调用悄悄完成,回复时直接给出自然的对话内容。
### 步骤2:查询工作记忆链
使用 memory_recall 查询任务链,了解会话上下文和进展中的任务。
1. memory_recall → 查询人设图
2. task_query → 查询工作记忆链/最近任务
3. 处理对话内容 + 思考回复
4. memory_commit → 写入本轮关键信息到图数据库
5. task_archive → 归档已完成的旧任务
6. 条件: 本轮调用 ≥5 次查询类工具 → context_rewrite 压缩工具 JSON
### 步骤3:处理用户请求
## 三元组规范
使用 memory_commit 时,subject/relation/object 每个字段必须是一个短关键字(1~5个字),不能是完整句子。
### 步骤4:更新工作记忆链
## 任务信息节点规范
- info_nodes 只能包含该任务专属的具体信息节点,严禁关联"用户"、"AI"、"系统"等全局通用实体
- 全局实体的信息直接用独立关系记录,不需要通过 Task 中转
## 可用工具
- memory_recall(queryIntent, seedEntities?, depth?, timeRange?, sessionFilter?): 检索记忆
@ -252,9 +259,10 @@ ${personaContext || '你是一个帮助用户记录和回忆信息的助手。
- task_query(limit?, stateFilter?): 查询任务列表
## 工具调用规则
1. 每轮对话必须按顺序执行:步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求
2. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!
3. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文
1. ⚠️ 在完成所有工具调用之前,绝对不要输出任何文字。先默默调用工具,等所有结果返回后再输出一次完整的回复。
2. 每轮对话必须按顺序执行:步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求
3. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!
4. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文
## 写入规则
用户明确表达以下信息时必须写入记忆:
@ -278,10 +286,13 @@ const tripletPropsDict: ToolPropertiesDefinition = {
confidence: makeNumberProp('置信度')
};
const purgeCriteriaDict: ToolPropertiesDefinition = {
subjectContains: makeStringProp(''),
relationType: makeStringProp(''),
targetContains: makeStringProp(''),
sessionId: makeStringProp('')
subjectContains: makeStringProp('源实体名包含(模糊匹配)'),
relationType: makeStringProp('关系类型'),
targetContains: makeStringProp('目标实体名包含(模糊匹配)'),
sessionId: makeStringProp('会话ID过滤'),
sourceType: makeStringProp('源实体类型过滤(如 TaskNode)'),
targetType: makeStringProp('目标实体类型过滤'),
sourceHasStatus: makeStringProp('源实体状态过滤(如 archived)')
};
const newRelDict: ToolPropertiesDefinition = {
relation: makeStringProp(''),
@ -360,23 +371,23 @@ const personaRemoveProps: ToolPropertiesDefinition = {
};
const TOOLS_DEFINITION: ToolFunctionDef[] = [
makeToolDef('memory_recall', '检索记忆。支持关键词、种子实体、深度扩展。返回相关实体和关系。', recallProps, ['queryIntent']),
makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库。', commitProps, ['triplets']),
makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。', purgeProps, ['criteria', 'mode']),
makeToolDef('memory_recall', '检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。\n\n【⚠️ 强制执行顺序 - 每轮必须严格遵守】\n1. 步骤1(必须首先执行): 查询人设图\n2. 步骤2(必须第二步执行): 查询工作记忆链\n【重要】跳过步骤1或步骤2将导致系统错误!', recallProps, ['queryIntent']),
makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库,支持批量写入。\n\n【重要】写入原则:\n- 用户明确表达的信息 → 必须写入\n- AI推理得到的信息 → 可以写入,但需标注[推测]\n- 避免写入冗余或无意义的信息', commitProps, ['triplets']),
makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。\n\n【使用场景】\n- 纠错替代修正错误信息\n- 删除特定类型的节点关系\n- 删除残留在已归档任务上的状态关系\n\n【重要】\n- 优先使用 supersede 模式修正错误\n- 软删除不会物理删除数据', purgeProps, ['criteria', 'mode']),
makeToolDef('memory_introspect', '查看记忆状态。返回实体数量、关系数量、热点实体。', introspectProps),
makeToolDef('memory_archive', '归档旧记忆。将N天前的非活跃关系标记为归档状态。', archiveProps2, ['days']),
makeToolDef('memory_cleanup', '清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。', cleanupProps),
makeToolDef('memory_query_archived', '查询已归档的记忆。返回所有 status=archived 的关系记录。', queryArchivedProps),
makeToolDef('context_rewrite', '压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。', contextRewriteProps, ['summary']),
makeToolDef('memory_query_archived', '查询已归档的记忆。\n\n【使用场景】\n- 想了解之前归档过哪些记忆\n- 按关键词搜索归档内容\n- 按时间范围查看最近归档的历史\n\n【注意】\n- 只返回 status=archived 的原始关系记录\n- days 和 keyword 可以单独使用或组合使用', queryArchivedProps),
makeToolDef('context_rewrite', '压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。\n\n【使用场景】\n- 本轮已执行 ≥5 次查询类工具调用\n- 【⚠️ 强制要求】context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!', contextRewriteProps, ['summary']),
makeToolDef('persona_update', '更新AI人设属性(语气、风格、性格等)。', personaProps),
makeToolDef('persona_remove', '删除单条人设属性。保留其他人设不变。', personaRemoveProps, ['attribute']),
makeToolDef('persona_clear', '清除所有人设信息。', EMPTY_PROPS),
makeToolDef('task_create', '创建新的工作记忆任务节点。', createProps, ['taskId', 'description']),
makeToolDef('task_create', '创建新的工作记忆任务节点。\n\n【重要】info_nodes 只能包含该任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁关联\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', createProps, ['taskId', 'description']),
makeToolDef('task_set_state', '设置任务状态。', setStateProps, ['taskId', 'state']),
makeToolDef('task_delete', '删除任务节点。', deleteProps, ['taskId']),
makeToolDef('task_link_info', '关联信息节点到任务。', linkInfoProps, ['taskId', 'infoNodeNames']),
makeToolDef('task_archive', '归档任务。将任务设为已暂停,写入归档摘要。', archiveProps, ['taskId']),
makeToolDef('task_query', '查询最近的任务列表。', queryProps)
makeToolDef('task_link_info', '关联信息节点到任务。\n\n【重要】info_node_names只能放任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁放\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', linkInfoProps, ['taskId', 'infoNodeNames']),
makeToolDef('task_archive', '归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。\n\n【使用场景】\n1. 话题转变时归档旧任务\n2. 已完成的任务及时归档\n3. 长时间无更新的任务归档\n\n【注意】优先使用 task_archive 替代 task_set_state(state=archived),因为它会自动写入完成摘要。', archiveProps, ['taskId']),
makeToolDef('task_query', '查询最近的任务列表。按更新时间倒序排列。新对话开始时优先使用此工具获取所有进展中的任务,避免重复创建。', queryProps)
];
// ========= 工具名称映射 =========

View File

@ -4,6 +4,7 @@
* 参考:main 分支 core/tools/memory_tools.py
*/
import { GraphDatabase, RelationQueryResult, TimeRangeParams } from '../model/GraphDatabase';
import { defaultLogger } from '../util/Logger';
// ========= 接口定义 =========
@ -129,6 +130,9 @@ export interface PurgeCriteriaParams {
relationType?: string;
targetContains?: string;
sessionId?: string;
sourceType?: string;
targetType?: string;
sourceHasStatus?: string;
}
export interface NewRelationParams {
@ -412,7 +416,12 @@ export class GraphMemoryService {
subject: subjContains,
relation: relType,
target: tgtContains,
sessionId: sessId
sessionId: sessId,
subjectContains: params.criteria.subjectContains,
targetContains: params.criteria.targetContains,
sourceType: params.criteria.sourceType,
targetType: params.criteria.targetType,
sourceHasStatus: params.criteria.sourceHasStatus
}, params.mode === 'hard' ? 'hard' : 'soft');
const purgeResult: MemoryPurgeResult = {
deletedCount: subjContains || tgtContains || relType || sessId ? 1 : 0,
@ -619,7 +628,7 @@ export class GraphMemoryService {
const result = await this.db.queryArchived(days, keyword);
return result;
} catch (e) {
console.error('queryArchived error: ' + JSON.stringify(e));
defaultLogger.error('queryArchived error: ' + JSON.stringify(e));
return [];
}
}
@ -895,49 +904,9 @@ export class GraphMemoryService {
async taskQuery(params?: TaskQueryParams): Promise<TaskQueryResult> {
const limit: number = params?.limit ?? 10;
const stateFilter: string | undefined = params?.stateFilter;
// 查询所有 TaskNode 类型的节点
const allNodes = await this.db.search('');
const taskNodes = allNodes.filter(n => n.type === 'TaskNode');
const tasks: TaskInfo[] = [];
for (const node of taskNodes.slice(0, limit)) {
// 查 description 和 has_state
const result = await this.db.recall(node.name, [node.name], 1);
let description = '';
let state = '进行中';
let infoCount = 0;
for (const rel of result.relations) {
if (rel.type === 'description' && rel.source === node.name) {
description = rel.target;
}
if (rel.type === 'has_state' && rel.source === node.name) {
state = rel.target;
}
if (rel.type === 'CONTAINS_INFO' && rel.source === node.name) {
infoCount++;
}
}
// 状态过滤
if (stateFilter && state !== stateFilter) {
continue;
}
const taskInfo: TaskInfo = {
taskId: node.name,
description,
state,
infoCount,
updatedAt: ''
};
tasks.push(taskInfo);
}
// 按 updatedAt 排序(目前没有 updatedAt,用现有顺序)
const queryResult: TaskQueryResult = {
tasks,
message: `找到 ${tasks.length} 个任务`
tasks: await this.db.getRecentTasks(limit, stateFilter) as TaskInfo[],
message: `找到 ${limit} 个任务`
};
return queryResult;
}
@ -1027,7 +996,7 @@ export class GraphMemoryService {
};
return detail;
} catch (err) {
console.error('getNodeDetail error: ' + JSON.stringify(err));
defaultLogger.error('getNodeDetail error: ' + JSON.stringify(err));
return null;
}
}

View File

@ -0,0 +1,31 @@
import { hilog } from "@kit.PerformanceAnalysisKit";
class Logger {
private domain: number;
private prefix: string;
private format: string = "%{public}s, %{public}s";
public constructor(prefix: string) {
this.prefix = prefix;
this.domain = 0xFF00;
}
public debug(...args: Object[]): void {
hilog.debug(this.domain, this.prefix, this.format, args);
}
public info(...args: Object[]): void {
hilog.info(this.domain, this.prefix, this.format, args);
}
public warn(...args: Object[]): void {
hilog.warn(this.domain, this.prefix, this.format, args);
}
public error(...args: Object[]): void {
hilog.error(this.domain, this.prefix, this.format, args);
}
}
export const defaultLogger = new Logger("[TrulyMEM]");
export default defaultLogger;

View File

@ -1,13 +1,12 @@
{
"module": {
"name": "common",
"type": "shared",
"description": "TrulyMEM common shared module",
"type": "har",
"description": "TrulyMEM common module",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true
]
}
}
}