mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-10-04 00:14:26 +00:00
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:
17
common/BuildProfile.ets
Normal file
17
common/BuildProfile.ets
Normal 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;
|
||||
}
|
||||
@ -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
1
common/common
Symbolic link
@ -0,0 +1 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/common
|
||||
6
common/hvigorfile.ts
Normal file
6
common/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
936
common/src/main/ets/model/GraphDatabase.ets.bak
Normal file
936
common/src/main/ets/model/GraphDatabase.ets.bak
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
];
|
||||
|
||||
// ========= 工具名称映射 =========
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
31
common/src/main/ets/util/Logger.ets
Normal file
31
common/src/main/ets/util/Logger.ets
Normal 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;
|
||||
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user