chore: update harmonyos source files

This commit is contained in:
root
2026-04-29 23:30:02 +08:00
parent ce7d3b532f
commit 45c098d10b
14 changed files with 2634 additions and 141 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
# Build cache
.hvigor/
entry/build/default/
trulymem-core/build/default/
trulymem-core/.preview/

1
core/web_config.json Normal file
View File

@ -0,0 +1 @@
{"SECRET_KEY": "3a38a0f46a76673154b491a7b061c38f2f6a55489078ae7b5d34e94e75fc0534"}

View File

@ -0,0 +1,132 @@
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
const THEME_COLOR = '#7C4DFF';
@Component
export struct ImmersiveTabNavigation {
@State currentIndex: number = 0;
@BuilderParam contentBuilder: () => void;
onTabChange?: (index: number) => void;
private windowFocused: boolean = true;
private bottomAvoidHeight: number = 0;
aboutToAppear() {
const mainWindow = AppStorage.get<window.Window>('main_window');
if (mainWindow) {
try {
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);
}
}
}
triggerTabSwitchFeedback(index: number) {
this.currentIndex = index;
AppStorage.setOrCreate('global_theme_color', THEME_COLOR);
this.onTabChange?.(index);
}
@Builder
tabBarBuilder(index: number, icon: string, label: string) {
Column() {
if (this.currentIndex === index && this.windowFocused) {
Circle()
.width(32)
.height(32)
.backgroundColor(`${THEME_COLOR}33`)
.blur(8)
.position({ x: '50%', y: '50%' })
.translate({ x: '-50%', y: '-50%' })
}
Text(icon)
.fontSize(20)
.opacity(this.currentIndex === index ? 1 : 0.5)
Text(label)
.fontSize(10)
.fontColor(this.currentIndex === index ? THEME_COLOR : '#999')
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
build() {
Stack() {
Column() {
this.contentBuilder()
}
.width('100%')
.height('100%')
Column() {
Stack() {
Column()
.width('100%')
.height('100%')
.backgroundBlurStyle(BlurStyle.Regular)
.borderRadius(24)
Column()
.width('100%')
.height('100%')
.backgroundColor(`${THEME_COLOR}0D`)
.borderRadius(24)
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['rgba(255,255,255,0.15)', 0.0], ['rgba(255,255,255,0.05)', 1.0]]
})
.borderRadius(24)
}
.width('100%')
.height('100%')
Tabs({ index: this.currentIndex }) {
TabContent() {
Column() {
Blank()
}
}
.tabBar(this.tabBarBuilder(0, '🌌', 'TrulyMEM'))
TabContent() {
Column() {
Blank()
}
}
.tabBar(this.tabBarBuilder(1, '⚙', '设置'))
}
.width('100%')
.height(64)
.barPosition(BarPosition.End)
.onChange((index: number) => {
this.triggerTabSwitchFeedback(index);
})
}
.width('92%')
.height(72)
.alignSelf(ItemAlign.Center)
.position({ y: `calc(100% - ${this.bottomAvoidHeight > 0 ? this.bottomAvoidHeight : 16}px - 72px)` })
.borderRadius(24)
.shadow({
radius: 20,
offsetY: -4,
color: 'rgba(0,0,0,0.15)'
})
}
.width('100%')
.height('100%')
.backgroundColor('#00000000')
}
}

View File

@ -1,7 +1,6 @@
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.window';
import Want from '@ohos.app.ability.Want';
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, param: AbilityConstant.LaunchParam): void {
@ -12,13 +11,34 @@ export default class EntryAbility extends UIAbility {
console.info('EntryAbility onDestroy');
}
onWindowStageCreate(windowStage: Window.WindowStage): void {
windowStage.loadContent('pages/Index', (err, data) => {
onWindowStageCreate(windowStage: window.WindowStage): void {
const windowClass: window.Window = windowStage.getMainWindowSync();
try {
windowClass.setWindowBackgroundColor('#00000000');
} catch (e) {
console.error('Failed to set background color: ' + (e as BusinessError).message);
}
try {
windowClass.setWindowSystemBarProperties({
statusBarColor: '#00000000',
navigationBarColor: '#00000000',
statusBarContentColor: '#FFFFFF',
navigationBarContentColor: '#FFFFFF'
});
} catch (e) {
console.error('Failed to set system bar: ' + (e as BusinessError).message);
}
AppStorage.setOrCreate('main_window', windowClass);
windowStage.loadContent('pages/Index', (err: BusinessError) => {
if (err.code) {
console.error('Failed to load content: ' + JSON.stringify(err));
console.error('Failed to load content: ' + err.message);
return;
}
console.info('Succeeded in loading content: ' + JSON.stringify(data));
console.info('Succeeded in loading content');
});
}

View File

@ -19,6 +19,7 @@ interface NodeData {
label: string;
type: string;
mentions: number;
depth?: number;
}
interface EdgeData {
@ -26,6 +27,9 @@ interface EdgeData {
to: number;
label: string;
weight: number;
depth?: number;
sessionId?: string;
turnId?: number;
}
interface GraphData {
@ -33,17 +37,86 @@ interface GraphData {
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 {
data: GraphData;
entities: RecallEntity[];
relations: RecallRelation[];
message: string;
}
interface BfsEntity {
id: number;
name: string;
type: string;
mentions: number;
depth: number;
}
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;
}
interface CleanupResult {
cleaned: number;
session_id?: string;
}
const STORE_CONFIG: relationalStore.StoreConfig = {
@ -80,91 +153,521 @@ export class GraphDatabase {
relation TEXT NOT NULL,
object_id INTEGER NOT NULL,
weight REAL DEFAULT 1.0,
created_at TEXT
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[]): Promise<void> {
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);
const objectId: number = await this.upsertNode(triplet.object);
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,
'created_at': new Date().toISOString()
'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 upsertNode(name: string): Promise<number> {
private async checkDuplicateRelation(subjectId: number, relation: string, objectId: number): Promise<number> {
if (!this.store) return -1;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.equalTo('name', name);
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.goToNextRow()) {
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': 'concept',
'created_at': new Date().toISOString(),
'updated_at': new Date().toISOString()
'type': entityType || 'concept',
'mentions': 1,
'created_at': now,
'updated_at': now
};
return await this.store.insert('nodes', bucket);
}
async recall(queryIntent: string, seedEntities?: string[]): Promise<RecallResult> {
async recall(queryIntent: string, seedEntities?: string[], depth: number = 2, sessionFilter?: string): Promise<RecallResult> {
if (!this.store) {
const empty: RecallResult = { data: { nodes: [], edges: [] } };
return empty;
return { entities: [], relations: [], message: 'Database not initialized' };
}
const nodes: NodeData[] = await this.getAllNodes();
const edges: EdgeData[] = await this.getAllEdges();
const result: RecallResult = { data: { nodes, edges } };
return result;
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);
for (let layer = 0; layer < depth && currentLayerIds.size > 0; layer++) {
const currentIds = Array.from(currentLayerIds);
const relations = await this.getRelationsForNodes(currentIds, sessionFilter);
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);
const nodeData = await this.getNodeById(newId);
if (nodeData) {
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} 条关系`
};
}
async purge(criteria: CriteriaData): Promise<void> {
if (!this.store) return;
private async getNodeById(id: number): Promise<NodeQueryResult | null> {
if (!this.store) return null;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
await this.store.delete(predicates);
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;
}
async introspect(): Promise<RecallResult> {
const nodes: NodeData[] = await this.getAllNodes();
const edges: EdgeData[] = await this.getAllEdges();
const result: RecallResult = { data: { nodes, edges } };
return result;
private async getRelationsForNodes(nodeIds: number[], sessionFilter?: string): Promise<RelationQueryResult[]> {
if (!this.store || nodeIds.length === 0) return [];
const relations: RelationQueryResult[] = [];
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);
}
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 = await this.getNodeById(sourceId);
const targetNode = 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);
}
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 = await this.getNodeById(sourceId);
const targetNode = 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 archive(days: number): Promise<void> {
// Archive old data - placeholder
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();
}
private async removeOrphanNodes(): Promise<number> {
if (!this.store) return 0;
let deleted = 0;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name']);
const nodesToCheck: number[] = [];
while (resultSet.goToNextRow()) {
nodesToCheck.push(resultSet.getLong(resultSet.getColumnIndex('id')));
}
resultSet.close();
for (const nodeId of nodesToCheck) {
const pred1: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
pred1.equalTo('subject_id', nodeId);
const rs1 = await this.store.query(pred1, ['id']);
const hasSourceRel = rs1.goToFirstRow();
rs1.close();
const pred2: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
pred2.equalTo('object_id', nodeId);
const rs2 = await this.store.query(pred2, ['id']);
const hasTargetRel = rs2.goToFirstRow();
rs2.close();
if (!hasSourceRel && !hasTargetRel) {
const deletePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
deletePred.equalTo('id', nodeId);
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> {
const result: CleanupResult = { cleaned: 0 };
return result;
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): Promise<void> {
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,
@ -173,19 +676,23 @@ export class GraphDatabase {
await this.store.insert('chat_records', bucket);
}
async getChatHistory(limit?: number): Promise<ChatMessage[]> {
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']);
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'))
content: resultSet.getString(resultSet.getColumnIndex('content')),
session_id: resultSet.getString(resultSet.getColumnIndex('session_id'))
};
messages.push(msg);
}
@ -234,4 +741,4 @@ export class GraphDatabase {
resultSet.close();
return edges;
}
}
}

View File

@ -1,100 +1,171 @@
import http from '@ohos.net.http';
import dataPreferences from '@ohos.data.preferences';
import { GraphDatabase } from '../model/GraphDatabase';
interface ChatMessage {
role: string;
content: string;
}
import { GraphMemoryService } from '../services/GraphMemoryService';
import { AIAgentService, ChatMessage, AgentResponse } from '../services/AIAgentService';
@Component
export struct ChatPage {
@State messages: ChatMessage[] = [];
@State inputText: string = '';
@Prop db: GraphDatabase;
@State toolCallLog: string = '';
@State isThinking: boolean = false;
private scrollController: Scroller = new Scroller();
private agentService?: AIAgentService;
async aboutToAppear() {
this.messages = await this.db.getChatHistory(50);
// 初始化图记忆服务和 Agent
const memoryService = new GraphMemoryService(this.db);
this.agentService = new AIAgentService(memoryService);
// 加载历史消息(兼容旧数据:无 session_id 时加载全部)
const rawHistory = await this.db.getChatHistory(50, this.agentService.getSessionId());
if (rawHistory.length === 0) {
// 新 session尝试加载旧消息
const legacyHistory = await this.db.getChatHistory(50);
this.messages = legacyHistory.map(m => {
const msg: ChatMessage = { role: m.role, content: m.content };
return msg;
});
} else {
this.messages = rawHistory.map(m => {
const msg: ChatMessage = { role: m.role, content: m.content };
return msg;
});
}
}
async sendMessage() {
if (!this.inputText.trim()) return;
if (!this.inputText.trim() || !this.agentService) return;
const userMessage: string = this.inputText;
this.inputText = '';
await this.db.saveChatMessage('user', userMessage);
const userMsg: ChatMessage = { role: 'user', content: userMessage };
this.messages = [...this.messages, userMsg];
// 添加用户消息
await this.db.saveChatMessage('user', userMessage, '', this.agentService.getSessionId());
this.messages = [...this.messages, { role: 'user', content: userMessage }];
const pref: dataPreferences.Preferences =
await dataPreferences.getPreferences(getContext(this), 'trulymem_config');
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
const model: string = String(await pref.get('model', 'deepseek-chat'));
const apiKey: string = String(await pref.get('api_key', ''));
// 显示 loading
this.isThinking = true;
this.toolCallLog = '';
try {
const httpRequest: http.HttpRequest = http.createHttp();
const response: http.HttpResponse = await httpRequest.request(baseUrl + '/chat/completions', {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
extraData: {
model: model,
messages: [{ role: 'user', content: userMessage }]
}
});
// 通过 Agent 发送消息
const agentResponse: AgentResponse = await this.agentService.sendMessage(userMessage);
if (response.responseCode === 200) {
const data: object = JSON.parse(String(response.result));
const choicesArr: object[] = data['choices'] as object[];
if (choicesArr && choicesArr.length > 0) {
const firstChoice: object = choicesArr[0];
const msgObj: object = firstChoice['message'] as object;
const assistantMessage: string = String(msgObj['content']);
await this.db.saveChatMessage('assistant', assistantMessage);
const aiMsg: ChatMessage = { role: 'assistant', content: assistantMessage };
this.messages = [...this.messages, aiMsg];
}
// 记录工具调用日志
if (agentResponse.toolCalls.length > 0) {
const logs: string[] = agentResponse.toolCalls.map(tc => `🛠 ${tc.name}: ${tc.message}`);
this.toolCallLog = logs.join('\n');
}
// 保存并显示 AI 回复
await this.db.saveChatMessage('assistant', agentResponse.content, this.toolCallLog, this.agentService.getSessionId());
this.messages = [...this.messages, { role: 'assistant', content: agentResponse.content }];
} catch (err) {
console.error('HTTP request failed: ' + JSON.stringify(err));
console.error('Agent request failed: ' + JSON.stringify(err));
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${err.message || JSON.stringify(err)}` }];
} finally {
this.isThinking = false;
}
}
build() {
Column() {
// 聊天列表
List() {
ForEach(this.messages, (msg: ChatMessage) => {
ListItem() {
Column() {
Text(msg.role).fontSize(12).fontColor('#666').width('100%')
Text(msg.content).fontSize(16).width('100%').margin({ top: 4 })
// 角色标识
Text(msg.role === 'user' ? '🧑 你' : '🤖 AI')
.fontSize(11)
.fontColor(msg.role === 'user' ? '#7C4DFF' : '#999')
.width('100%')
// 消息内容
Text(msg.content)
.fontSize(15)
.width('100%')
.margin({ top: 4 })
.fontColor('#FFFFFF')
}
.padding(12)
.backgroundColor(msg.role === 'user' ? '#E3F2FD' : '#F5F5F5')
.borderRadius(8)
.backgroundColor(msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)')
.borderRadius(12)
.border({ width: 1, color: msg.role === 'user' ? 'rgba(124,77,255,0.3)' : 'rgba(255,255,255,0.1)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 8 })
.width('100%')
.alignItems(HorizontalAlign.Start)
}
})
// loading 指示
if (this.isThinking) {
ListItem() {
Row() {
LoadingProgress()
.width(20)
.height(20)
.margin({ right: 8 })
.color('#7C4DFF')
Text('AI 思考中...')
.fontSize(13)
.fontColor('#7C4DFF')
}
.padding(12)
.backgroundColor('rgba(124,77,255,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(124,77,255,0.2)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 8 })
}
}
// 工具调用日志
if (this.toolCallLog && !this.isThinking) {
ListItem() {
Text(this.toolCallLog)
.fontSize(10)
.fontColor('#FF9800')
.backgroundColor('rgba(255,152,0,0.1)')
.padding(8)
.borderRadius(8)
.border({ width: 1, color: 'rgba(255,152,0,0.2)' })
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ left: 8, right: 8, bottom: 4 })
.lineHeight(16)
}
}
}
.width('100%')
.layoutWeight(1)
.backgroundColor('rgba(0,0,0,0.1)')
// 输入区
Row() {
TextArea({ text: this.inputText, placeholder: '输入消息...' })
.layoutWeight(1)
.onChange((v: string) => { this.inputText = v; })
.height(40)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
Button('发送')
.enabled(!this.isThinking)
.onClick(() => this.sendMessage())
.backgroundColor('#7C4DFF')
.borderRadius(8)
}
.width('100%')
.padding(8)
.backgroundColor('rgba(255,255,255,0.05)')
.backgroundBlurStyle(BlurStyle.Regular)
.border({ width: 1, color: 'rgba(124,77,255,0.2)', style: BorderStyle.Solid })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(26,27,46,0.95)')
}
}

View File

@ -1,22 +1,162 @@
/**
* GraphPage — 记忆星图页面
* 使用 WebView 显示 Three.js 3D 图可视化
* 通过 javaScriptProxy 与 WebView 双向通信
*/
import web_webview from '@ohos.web.webview';
import { GraphDatabase } from '../model/GraphDatabase';
import { GraphDatabase, RecallEntity } from '../model/GraphDatabase';
// ========= 图数据结构定义 =========
interface GraphNodeItem {
id: number;
label: string;
type: string;
mentions: number;
}
interface GraphEdgeItem {
id: number;
source: number;
target: number;
label: string;
relation: string;
}
// ========= WebView 原生桥接 =========
class NativeBridge {
private controller: web_webview.WebviewController;
private onRequestGraphData: () => void;
constructor(controller: web_webview.WebviewController, onRequestGraphData: () => void) {
this.controller = controller;
this.onRequestGraphData = onRequestGraphData;
}
onNodeClick(nodeId: number, nodeName: string): void {
console.info('Node clicked: id=' + nodeId + ', name=' + nodeName);
}
requestGraphData(): void {
console.info('requestGraphData called from WebView');
if (this.onRequestGraphData) {
this.onRequestGraphData();
}
}
}
// ========= GraphPage 组件 =========
@Component
export struct GraphPage {
private controller: web_webview.WebviewController = new web_webview.WebviewController();
@Prop db: GraphDatabase;
@State nodeCount: number = 0;
@State edgeCount: number = 0;
// 初始化桥接对象
private bridge: NativeBridge = new NativeBridge(this.controller, (): void => {
this.pushGraphDataToWebView();
});
/**
* 外部触发刷新图数据(聊天写入新记忆后调用)
*/
public async refreshGraphData(): Promise<void> {
await this.pushGraphDataToWebView();
}
/**
* 从数据库读取全量图数据,通过 runJavaScript 推送给 WebView
*/
private async pushGraphDataToWebView(): Promise<void> {
try {
// 获取所有节点
const allNodes: GraphNodeItem[] = await this.getAllNodesData();
// 获取所有活跃关系
const allEdges: GraphEdgeItem[] = await this.getAllEdgesData();
// 通过 JavaScript Bridge 推送数据
if (this.controller) {
const jsCode: string =
`window.loadGraphData(${JSON.stringify({ nodes: allNodes, edges: allEdges })});`;
this.controller.runJavaScript(jsCode);
}
this.nodeCount = allNodes.length;
this.edgeCount = allEdges.length;
} catch (err) {
console.error('pushGraphDataToWebView error: ' + JSON.stringify(err));
}
}
/**
* 从数据库查询所有节点
*/
private async getAllNodesData(): Promise<GraphNodeItem[]> {
const result = await this.db.search('');
const items: GraphNodeItem[] = result.map((r, idx): GraphNodeItem => {
const item: GraphNodeItem = {
id: idx + 1,
label: r.name,
type: r.type,
mentions: r.mentions
};
return item;
});
return items;
}
/**
* 从数据库查询所有活跃关系
*/
private async getAllEdgesData(): Promise<GraphEdgeItem[]> {
const recallResult = await this.db.recall('', [], 3);
const nameToId: Record<string, number> = {};
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
nameToId[e.name as string] = idx + 1;
});
const edgeItems: GraphEdgeItem[] = [];
for (let i = 0; i < recallResult.relations.length; i++) {
const r = recallResult.relations[i];
const sourceId: number | undefined = nameToId[r.source];
const targetId: number | undefined = nameToId[r.target];
if (sourceId !== undefined && targetId !== undefined) {
const edgeItem: GraphEdgeItem = {
id: i + 1,
source: sourceId,
target: targetId,
label: r.type,
relation: r.type
};
edgeItems.push(edgeItem);
}
}
return edgeItems;
}
build() {
Column() {
// WebView 显示 3D 星图
Web({ src: $rawfile('graph.html'), controller: this.controller })
.javaScriptAccess(true)
.width('100%')
.height('100%')
// 页面加载完成后推送数据
.onPageEnd(() => {
this.pushGraphDataToWebView();
})
// 注册原生桥接对象,供 WebView JavaScript 调用
.javaScriptProxy({
object: this.bridge,
name: 'nativeBridge',
methodList: ['onNodeClick'],
asyncMethodList: ['requestGraphData'],
controller: this.controller
})
}
.width('100%')
.height('100%')
.onAppear(() => {
this.controller.refresh();
})
}
}

View File

@ -1,35 +1,40 @@
import { MainPage } from './MainPage';
import { SettingsPage } from './SettingsPage';
import { GraphDatabase } from '../model/GraphDatabase';
import { ImmersiveTabNavigation } from '../components/ImmersiveTabNavigation';
@Entry
@Component
struct Index {
@State currentIndex: number = 0;
private db: GraphDatabase = new GraphDatabase();
@State currentIndex: number = 0;
private db: GraphDatabase = new GraphDatabase();
aboutToAppear() {
this.db.init(getContext(this));
aboutToAppear() {
this.db.init(getContext(this));
}
@Builder
tabContentBuilder() {
Column() {
if (this.currentIndex === 0) {
MainPage({ db: this.db })
} else {
SettingsPage()
}
}
.width('100%')
.height('100%')
}
build() {
Column() {
Tabs({ index: this.currentIndex, barPosition: BarPosition.End }) {
TabContent() {
MainPage({ db: this.db })
}
.tabBar('🌌 TrulyMEM')
TabContent() {
SettingsPage()
}
.tabBar('⚙ 设置')
}
.width('100%')
.height('100%')
.onChange((index: number) => { this.currentIndex = index; })
}
.width('100%')
.height('100%')
build() {
Stack() {
ImmersiveTabNavigation({
currentIndex: this.currentIndex,
onTabChange: (index: number): void => { this.currentIndex = index; },
contentBuilder: (): void => { this.tabContentBuilder(); }
})
}
.width('100%')
.height('100%')
}
}

View File

@ -9,13 +9,30 @@ export struct MainPage {
build() {
GridRow({ columns: { sm: 1, md: 2, lg: 2 }, gutter: { x: 8, y: 8 } }) {
GridCol({ span: { sm: 1, md: 1, lg: 1 } }) {
GraphPage({ db: this.db })
Stack() {
GraphPage({ db: this.db })
}
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.width('100%')
.height('100%')
}
GridCol({ span: { sm: 1, md: 1, lg: 1 } }) {
ChatPage({ db: this.db })
Stack() {
ChatPage({ db: this.db })
}
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.width('100%')
.height('100%')
}
}
.width('100%')
.height('100%')
.backgroundColor('#1A1B2E')
.backgroundBlurStyle(BlurStyle.Regular)
}
}

View File

@ -34,27 +34,93 @@ export struct SettingsPage {
}
build() {
Column() {
Text('API 配置').fontSize(24).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 16 })
Stack() {
// 主题色光晕背景
Column()
.width(200)
.height(200)
.backgroundColor('rgba(124,77,255,0.15)')
.blur(40)
.borderRadius(100)
.position({ x: '10%', y: '20%' })
Text('Base URL').fontSize(14).width('100%').margin({ left: 16 })
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
.onChange((v: string) => { this.onBaseUrlChange(v); })
.margin({ left: 16, right: 16, bottom: 12 })
Column() {
Text('API 配置')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 20, bottom: 16 })
Text('Model ID').fontSize(14).width('100%').margin({ left: 16 })
TextInput({ placeholder: 'deepseek-chat', text: this.model })
.onChange((v: string) => { this.onModelChange(v); })
.margin({ left: 16, right: 16, bottom: 12 })
// Base URL 设置项
Column() {
Text('Base URL')
.fontSize(14)
.fontColor('#FFFFFF')
.width('100%')
.margin({ bottom: 8 })
Text('API Key').fontSize(14).width('100%').margin({ left: 16 })
TextInput({ placeholder: 'sk-...', text: this.apiKey })
.type(InputType.Password)
.onChange((v: string) => { this.onApiKeyChange(v); })
.margin({ left: 16, right: 16, bottom: 12 })
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
.onChange((v: string) => { this.onBaseUrlChange(v); })
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
.height(40)
}
.padding(12)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ bottom: 12 })
// Model ID 设置项
Column() {
Text('Model ID')
.fontSize(14)
.fontColor('#FFFFFF')
.width('100%')
.margin({ bottom: 8 })
TextInput({ placeholder: 'deepseek-chat', text: this.model })
.onChange((v: string) => { this.onModelChange(v); })
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
.height(40)
}
.padding(12)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ bottom: 12 })
// API Key 设置项
Column() {
Text('API Key')
.fontSize(14)
.fontColor('#FFFFFF')
.width('100%')
.margin({ bottom: 8 })
TextInput({ placeholder: 'sk-...', text: this.apiKey })
.type(InputType.Password)
.onChange((v: string) => { this.onApiKeyChange(v); })
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
.height(40)
}
.padding(12)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
.backgroundBlurStyle(BlurStyle.Thin)
.margin({ bottom: 12 })
}
.padding(16)
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('rgba(26,27,46,0.95)')
.backgroundBlurStyle(BlurStyle.Regular)
}
}

View File

@ -0,0 +1,764 @@
/**
* AIAgentService - AI Agent 服务层
* 管理上下文感知的 AI 对话,注入图数据作为上下文,
* 解析 AI 返回中的记忆操作,调用 GraphMemoryService 执行
* 参考main 分支 core/graph_client.py
*/
import http from '@ohos.net.http';
import dataPreferences from '@ohos.data.preferences';
import { GraphMemoryService, EntityInfo, RelationInfo, TaskInfo, MemoryRecallParams, MemoryCommitParams, MemoryPurgeParams, TimeRangeParams, PurgeCriteriaParams, NewRelationParams, PersonaUpdateParams, TaskCreateParams, TaskSetStateParams, TaskDeleteParams, TaskLinkInfoParams, TaskArchiveParams, TaskQueryParams, TripletInput, PersonaQueryResult, TaskQueryResult, MemoryRecallResult } from './GraphMemoryService';
export interface ChatMessage {
role: string;
content: string;
}
export interface AgentResponse {
content: string;
toolCalls: ToolCallResult[];
}
export interface ToolCallResult {
name: string;
success: boolean;
message: string;
}
// ========= 工具定义类型 =========
// Concrete interface for tool property definitions (replaces Record<string, T>)
interface ToolPropertiesDefinition {
days?: ToolParamProperty;
queryIntent?: ToolParamProperty;
seedEntities?: ToolParamProperty;
depth?: ToolParamProperty;
timeRange?: ToolParamProperty;
sessionFilter?: ToolParamProperty;
triplets?: ToolParamProperty;
entityTypes?: ToolParamProperty;
sessionId?: ToolParamProperty;
turnId?: ToolParamProperty;
criteria?: ToolParamProperty;
mode?: ToolParamProperty;
newRelation?: ToolParamProperty;
tone?: ToolParamProperty;
style?: ToolParamProperty;
personality?: ToolParamProperty;
catchphrase?: ToolParamProperty;
background?: ToolParamProperty;
taskId?: ToolParamProperty;
description?: ToolParamProperty;
infoNodes?: ToolParamProperty;
state?: ToolParamProperty;
deleteInfoNodes?: ToolParamProperty;
infoNodeNames?: ToolParamProperty;
summary?: ToolParamProperty;
limit?: ToolParamProperty;
stateFilter?: ToolParamProperty;
subject?: ToolParamProperty;
relation?: ToolParamProperty;
object?: ToolParamProperty;
confidence?: ToolParamProperty;
subjectContains?: ToolParamProperty;
relationType?: ToolParamProperty;
targetContains?: ToolParamProperty;
target?: ToolParamProperty;
}
interface ToolParamProperty {
type: string;
description: string;
items?: ToolParamProperty;
properties?: ToolPropertiesDefinition;
required?: string[];
enum?: string[];
}
interface ToolParamDecl {
type: string;
properties: ToolPropertiesDefinition;
required?: string[];
}
interface ToolFunctionDecl {
name: string;
description: string;
parameters: ToolParamDecl;
}
interface ToolFunctionDef {
type: string;
function: ToolFunctionDecl;
}
// ========= API 请求/响应结构 =========
interface ApiRequestMessage {
role: string;
content: string;
}
interface ApiRequest {
model: string;
messages: ApiRequestMessage[];
tools?: ToolFunctionDef[];
tool_choice?: string;
}
interface ApiToolCall {
id: string;
type: string;
function: ToolFunctionCall;
}
interface ToolFunctionCall {
name: string;
arguments: string;
}
interface ApiChoiceMessage {
content?: string;
tool_calls?: ApiToolCall[];
}
interface ApiChoice {
message: ApiChoiceMessage;
}
interface ApiResponse {
choices: ApiChoice[];
}
// ========= 内部结果类型 =========
interface ExecuteToolResult {
name: string;
success: boolean;
message: string;
}
interface BuildContextBlockParams {
persona: Record<string, string>;
found: boolean;
entities: EntityInfo[];
relations: RelationInfo[];
message: string;
}
// ========= 服务方法参数类型 =========
// ========= 工具定义辅助函数 =========
function makeStringProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'string', description: description };
return result;
}
function makeIntegerProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'integer', description: description };
return result;
}
function makeObjectProp(description: string, props: ToolPropertiesDefinition, required?: string[]): ToolParamProperty {
const param: ToolParamProperty = { type: 'object', description: description };
param.properties = props;
if (required && required.length > 0) {
param.required = required;
}
return param;
}
function makeArrayProp(description: string, item: ToolParamProperty): ToolParamProperty {
const result: ToolParamProperty = { type: 'array', description: description, items: item };
return result;
}
function makeBoolProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'boolean', description: description };
return result;
}
function makeNumberProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'number', description: description };
return result;
}
function makeEnumProp(description: string, enumValues: string[]): ToolParamProperty {
const result: ToolParamProperty = { type: 'string', description: description, enum: enumValues };
return result;
}
function makeToolDef(name: string, description: string, properties: ToolPropertiesDefinition, required?: string[]): ToolFunctionDef {
const params: ToolParamDecl = { type: 'object', properties: properties };
const func: ToolFunctionDecl = { name: name, description: description, parameters: params };
const tool: ToolFunctionDef = { type: 'function', function: func };
if (required && required.length > 0) {
tool.function.parameters.required = required;
}
return tool;
}
/**
* 系统提示词 — AI 人设 + 图记忆使用说明
*/
function buildSystemPrompt(personaContext: string): string {
return `你是 TrulyMEMTrue Memory——一个拥有真实记忆的 AI 助手。
## 核心身份
${personaContext || '你是一个帮助用户记录和回忆信息的助手。你的核心能力是基于图数据库的记忆系统。'}
## 记忆系统
你的记忆存储在图数据库中,每个记忆都是「实体 - 关系 - 实体」的三元组形式。
每轮对话开始,你都会按以下顺序操作:
### 步骤1查询人设
使用 memory_recall 查询当前人设,确保回复风格一致。
### 步骤2查询工作记忆链
使用 memory_recall 查询任务链,了解会话上下文和进展中的任务。
### 步骤3处理用户请求
### 步骤4更新工作记忆链
## 可用工具
- memory_recall(queryIntent, seedEntities?, depth?, timeRange?, sessionFilter?): 检索记忆
- memory_commit(triplets, entityTypes?, sessionId?, turnId?): 写入记忆
- memory_purge(criteria, mode, newRelation?): 删除/修正记忆
- persona_update(tone?, style?, personality?, catchphrase?, background?): 更新人设
- persona_clear(): 清除人设
- task_create(taskId, description, infoNodes?): 创建任务
- task_set_state(taskId, state): 设置任务状态
- task_delete(taskId, deleteInfoNodes?): 删除任务
- task_link_info(taskId, infoNodeNames): 关联信息节点
- task_archive(taskId, summary?): 归档任务
- task_query(limit?, stateFilter?): 查询任务列表
## 写入规则
用户明确表达以下信息时必须写入记忆:
- 偏好、兴趣
- 个人信息(工作、项目、学习)
- 计划安排
- 当前状态
- 结论性事实
推理得到的信息可以写入但需标注 [推测]。`;
}
// ========= 工具定义 =========
// Pre-typed property dictionaries for tool definitions
const recallTimeRangeDict: ToolPropertiesDefinition = { days: makeIntegerProp('最近N天') };
const tripletPropsDict: ToolPropertiesDefinition = {
subject: makeStringProp('主体'),
relation: makeStringProp('关系'),
object: makeStringProp('客体'),
confidence: makeNumberProp('置信度')
};
const purgeCriteriaDict: ToolPropertiesDefinition = {
subjectContains: makeStringProp(''),
relationType: makeStringProp(''),
targetContains: makeStringProp(''),
sessionId: makeStringProp('')
};
const newRelDict: ToolPropertiesDefinition = {
relation: makeStringProp(''),
target: makeStringProp('')
};
const EMPTY_PROPS: ToolPropertiesDefinition = {};
const recallProps: ToolPropertiesDefinition = {
queryIntent: makeStringProp('查询意图,支持逗号分隔多个关键词'),
seedEntities: makeArrayProp('种子实体(可选)', makeStringProp('')),
depth: makeIntegerProp('搜索深度默认2'),
timeRange: makeObjectProp('时间范围(可选)', recallTimeRangeDict),
sessionFilter: makeStringProp('会话ID过滤可选')
};
const commitProps: ToolPropertiesDefinition = {
triplets: makeArrayProp('三元组列表', makeObjectProp('', tripletPropsDict, ['subject', 'relation', 'object'])),
entityTypes: makeObjectProp('实体类型映射(可选)', EMPTY_PROPS),
sessionId: makeStringProp('会话ID可选'),
turnId: makeIntegerProp('轮次ID可选')
};
const purgeProps: ToolPropertiesDefinition = {
criteria: makeObjectProp('删除条件', purgeCriteriaDict),
mode: makeEnumProp('删除模式soft逻辑删除, hard物理删除, supersede纠错替代', ['soft', 'hard', 'supersede']),
newRelation: makeObjectProp('替代关系supersede模式用', newRelDict)
};
const personaProps: ToolPropertiesDefinition = {
tone: makeStringProp('语气'),
style: makeStringProp('风格'),
personality: makeStringProp('性格'),
catchphrase: makeStringProp('口头禅'),
background: makeStringProp('背景')
};
const createProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
description: makeStringProp('任务描述'),
infoNodes: makeArrayProp('关联的信息节点名称列表', makeStringProp(''))
};
const setStateProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
state: makeEnumProp('任务状态', ['进行中', '已完成', '已暂停', '已取消'])
};
const deleteProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
deleteInfoNodes: makeBoolProp('是否删除关联的信息节点')
};
const linkInfoProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
infoNodeNames: makeArrayProp('信息节点名称列表', makeStringProp(''))
};
const archiveProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
summary: makeStringProp('归档摘要')
};
const queryProps: ToolPropertiesDefinition = {
limit: makeIntegerProp('返回数量默认10'),
stateFilter: makeStringProp('状态过滤: 进行中/已完成/已暂停/已取消/archived')
};
const TOOLS_DEFINITION: ToolFunctionDef[] = [
makeToolDef('memory_recall', '检索记忆。支持关键词、种子实体、深度扩展。返回相关实体和关系。', recallProps, ['queryIntent']),
makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库。', commitProps, ['triplets']),
makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。', purgeProps, ['criteria', 'mode']),
makeToolDef('persona_update', '更新AI人设属性语气、风格、性格等。', personaProps),
makeToolDef('persona_clear', '清除所有人设信息。', EMPTY_PROPS),
makeToolDef('task_create', '创建新的工作记忆任务节点。', 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)
];
// ========= 工具名称映射 =========
// Types for executeTool generic args
type ToolStateArg = '进行中' | '已完成' | '已暂停' | '已取消';
type ToolHandlerName =
| 'memoryRecal'
| 'memoryCommit'
| 'memoryPurge'
| 'personaUpdate'
| 'personaClear'
| 'taskCreate'
| 'taskSetState'
| 'taskDelete'
| 'taskLinkInfo'
| 'taskArchive'
| 'taskQuery';
const TOOL_HANDLER_MAP: Record<string, ToolHandlerName> = {
'memory_recall': 'memoryRecal',
'memory_commit': 'memoryCommit',
'memory_purge': 'memoryPurge',
'persona_update': 'personaUpdate',
'persona_clear': 'personaClear',
'task_create': 'taskCreate',
'task_set_state': 'taskSetState',
'task_delete': 'taskDelete',
'task_link_info': 'taskLinkInfo',
'task_archive': 'taskArchive',
'task_query': 'taskQuery',
};
// ========= AIAgentService =========
export class AIAgentService {
private memoryService: GraphMemoryService;
private currentSessionId: string;
private turnCounter: number = 0;
constructor(memoryService: GraphMemoryService, sessionId?: string) {
this.memoryService = memoryService;
this.currentSessionId = sessionId || `session-hm-${Date.now()}`;
}
getSessionId(): string {
return this.currentSessionId;
}
/**
* 发送消息 — 完整的 Agent 流程
* 1. 查询人设
* 2. 查询工作记忆链
* 3. 注入上下文后请求 AI
* 4. 处理 tool_calls
* 5. 返回最终回复
*/
async sendMessage(userInput: string): Promise<AgentResponse> {
this.turnCounter++;
// === 步骤1+2: 获取上下文 ===
const personaResult = await this.memoryService.personaQuery();
const personaContext: string = personaResult.found ? this.formatPersona(personaResult.persona) : '';
const recallParams: MemoryRecallParams = {
queryIntent: 'TaskNode,工作记忆,任务链',
depth: 2
};
const taskResult = await this.memoryService.memoryRecall(recallParams);
// === 读取 API 配置 ===
const context = getContext(this);
const pref = await dataPreferences.getPreferences(context, 'trulymem_config');
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
const model: string = String(await pref.get('model', 'deepseek-chat'));
const apiKey: string = String(await pref.get('api_key', ''));
if (!apiKey) {
const noKeyResponse: AgentResponse = {
content: '⚠️ API Key 未配置,请先在设置页填写 API Key。',
toolCalls: []
};
return noKeyResponse;
}
// === 构建上下文丰富的消息 ===
const systemPrompt: string = buildSystemPrompt(personaContext);
const contextBlock: string = this.buildContextBlock(personaResult, taskResult);
const sysMsg: ApiRequestMessage = { role: 'system' as string, content: systemPrompt };
const userMsg: ApiRequestMessage = { role: 'user' as string, content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
const messages: ApiRequestMessage[] = [sysMsg, userMsg];
// === 步骤3: 请求 AI ===
const response: ApiResponse = await this.callApi(messages, baseUrl, model, apiKey);
const toolCalls: ToolCallResult[] = [];
// === 步骤4: 处理 tool_calls ===
if (response.choices && response.choices.length > 0) {
const choice: ApiChoice = response.choices[0];
const aiMessage: ApiChoiceMessage = choice.message;
// 处理函数调用
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
for (const tc of aiMessage.tool_calls) {
const handlerName: ToolHandlerName | undefined = TOOL_HANDLER_MAP[tc.function.name];
if (handlerName) {
const args: Record<string, Object> = JSON.parse(tc.function.arguments);
const result: ToolCallResult = await this.executeTool(handlerName, args);
toolCalls.push(result);
} else {
const unknownToolResult: ToolCallResult = {
name: tc.function.name,
success: false,
message: '未知工具'
};
toolCalls.push(unknownToolResult);
}
}
// 有 tool_calls 时需要再次请求 AI带上工具执行结果
const followUpSystemMsg: ApiRequestMessage = { role: 'system', content: systemPrompt };
const followUpUserMsg: ApiRequestMessage = { role: 'user', content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
const followUpAssistantMsg: ApiRequestMessage = {
role: 'assistant',
content: aiMessage.content || '(已执行记忆操作)',
};
const toolResultsMessages: ApiRequestMessage[] = [
followUpSystemMsg,
followUpUserMsg,
followUpAssistantMsg,
];
for (const tc of aiMessage.tool_calls) {
const callResult: ToolCallResult | undefined = toolCalls.find(r => r.name === tc.function.name);
const toolResultMsg: string = callResult ? callResult.message : '完成';
const toolResultMessage: ApiRequestMessage = {
role: 'tool',
content: `工具 ${tc.function.name} 执行结果: ${toolResultMsg}`
};
toolResultsMessages.push(toolResultMessage);
}
const finalResponse: ApiResponse = await this.callApi(toolResultsMessages, baseUrl, model, apiKey);
if (finalResponse.choices && finalResponse.choices.length > 0) {
const content: string = finalResponse.choices[0].message.content || '';
const finalResult: AgentResponse = { content, toolCalls };
return finalResult;
}
}
// 普通回复(无 tool_calls
const content: string = aiMessage.content || '';
const noToolResponse: AgentResponse = { content, toolCalls };
return noToolResponse;
}
const noResponse: AgentResponse = {
content: 'AI 无响应',
toolCalls
};
return noResponse;
}
/**
* 请求 DeepSeek API
*/
private async callApi(
messages: ApiRequestMessage[],
baseUrl: string,
model: string,
apiKey: string
): Promise<ApiResponse> {
const httpRequest = http.createHttp();
try {
const resp = await httpRequest.request(baseUrl + '/chat/completions', {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
extraData: {
model: model,
messages: messages,
tools: TOOLS_DEFINITION,
tool_choice: 'auto'
},
expectDataType: http.HttpDataType.OBJECT,
readTimeout: 60000
});
if (resp.responseCode === 200) {
return resp.result as ApiResponse;
}
const errorMsg: string = `API 请求失败: HTTP ${resp.responseCode}`;
throw new Error(errorMsg);
} finally {
httpRequest.destroy();
}
}
/**
* 执行工具调用
*/
private async executeTool(name: ToolHandlerName, args: Record<string, Object>): Promise<ToolCallResult> {
try {
switch (name) {
case 'memoryRecal': {
const recallArgs: MemoryRecallParams = {
queryIntent: args.queryIntent as string,
seedEntities: args.seedEntities as string[],
depth: (args.depth as number) ?? 2,
timeRange: args.timeRange as TimeRangeParams,
sessionFilter: args.sessionFilter as string
};
const recallResult = await this.memoryService.memoryRecall(recallArgs);
const result: ToolCallResult = {
name: 'memory_recall',
success: true,
message: `找到 ${recallResult.entities.length} 个实体, ${recallResult.relations.length} 条关系`
};
return result;
}
case 'memoryCommit': {
const commitParams: MemoryCommitParams = {
triplets: args.triplets as TripletInput[],
entityTypes: args.entityTypes as Record<string, string>,
sessionId: (args.sessionId as string) || this.currentSessionId,
turnId: (args.turnId as number) || this.turnCounter
};
const commitResult = await this.memoryService.memoryCommit(commitParams);
const result: ToolCallResult = {
name: 'memory_commit',
success: true,
message: `已写入 ${commitResult.committedCount} 条记忆`
};
return result;
}
case 'memoryPurge': {
const purgeArgs: MemoryPurgeParams = {
criteria: args.criteria as PurgeCriteriaParams,
mode: args.mode as 'soft' | 'hard' | 'supersede',
newRelation: args.newRelation as NewRelationParams
};
const purgeResult = await this.memoryService.memoryPurge(purgeArgs);
const result: ToolCallResult = {
name: 'memory_purge',
success: true,
message: purgeResult.message
};
return result;
}
case 'personaUpdate': {
const personaParams: PersonaUpdateParams = {
tone: args.tone as string,
style: args.style as string,
personality: args.personality as string,
catchphrase: args.catchphrase as string,
background: args.background as string
};
const puResult = await this.memoryService.personaUpdate(personaParams);
const result: ToolCallResult = {
name: 'persona_update',
success: puResult.success,
message: puResult.message
};
return result;
}
case 'personaClear': {
const pcResult = await this.memoryService.personaClear();
const result: ToolCallResult = {
name: 'persona_clear',
success: pcResult.success,
message: pcResult.message
};
return result;
}
case 'taskCreate': {
const createParams: TaskCreateParams = {
taskId: args.taskId as string,
description: args.description as string,
infoNodes: args.infoNodes as string[]
};
const tcResult = await this.memoryService.taskCreate(createParams);
const result: ToolCallResult = {
name: 'task_create',
success: tcResult.success,
message: tcResult.message
};
return result;
}
case 'taskSetState': {
const setStateParams: TaskSetStateParams = {
taskId: args.taskId as string,
state: args.state as ToolStateArg
};
const tsResult = await this.memoryService.taskSetState(setStateParams);
const result: ToolCallResult = {
name: 'task_set_state',
success: tsResult.success,
message: tsResult.message
};
return result;
}
case 'taskDelete': {
const deleteParams: TaskDeleteParams = {
taskId: args.taskId as string,
deleteInfoNodes: (args.deleteInfoNodes as boolean) !== false
};
const tdResult = await this.memoryService.taskDelete(deleteParams);
const result: ToolCallResult = {
name: 'task_delete',
success: tdResult.success,
message: tdResult.message
};
return result;
}
case 'taskLinkInfo': {
const linkInfoParams: TaskLinkInfoParams = {
taskId: args.taskId as string,
infoNodeNames: args.infoNodeNames as string[]
};
const tliResult = await this.memoryService.taskLinkInfo(linkInfoParams);
const result: ToolCallResult = {
name: 'task_link_info',
success: tliResult.success,
message: tliResult.message
};
return result;
}
case 'taskArchive': {
const archiveParams: TaskArchiveParams = {
taskId: args.taskId as string,
summary: args.summary as string
};
const taResult = await this.memoryService.taskArchive(archiveParams);
const result: ToolCallResult = {
name: 'task_archive',
success: taResult.success,
message: taResult.message
};
return result;
}
case 'taskQuery': {
const tqResult = await this.memoryService.taskQuery({
limit: args.limit as number,
stateFilter: args.stateFilter as string
});
const result: ToolCallResult = {
name: 'task_query',
success: true,
message: `找到 ${tqResult.tasks.length} 个任务`
};
return result;
}
default: {
const defaultResult: ToolCallResult = {
name: name as string,
success: false,
message: '未实现的工具'
};
return defaultResult;
}
}
} catch (e) {
const errorMessage: string = (e as Error).message || '';
const errorResult: ToolCallResult = {
name: name as string,
success: false,
message: `执行失败: ${errorMessage}`
};
return errorResult;
}
}
/**
* 格式化人设数据为文本
*/
private formatPersona(persona: Record<string, string>): string {
const parts: string[] = [];
const keys: string[] = Object.keys(persona);
for (let i = 0; i < keys.length; i++) {
const key: string = keys[i];
const val: string = persona[key];
parts.push(`${key}: ${val}`);
}
return parts.length > 0 ? parts.join('') : '';
}
/**
* 构建上下文注入块
*/
private buildContextBlock(
personaResult: PersonaQueryResult,
taskResult: MemoryRecallResult
): string {
const blocks: string[] = [];
if (personaResult.found) {
blocks.push(`【当前人设】\n${this.formatPersona(personaResult.persona)}`);
}
if (taskResult.entities.length > 0) {
const entitySample: EntityInfo[] = taskResult.entities.slice(0, 5);
const entitiesStr: string = JSON.stringify(entitySample);
blocks.push(`【工作记忆】\n${taskResult.message}\n${entitiesStr}`);
}
return blocks.length > 0 ? blocks.join('\n\n') : '【新对话】';
}
}

View File

@ -0,0 +1,751 @@
/**
* GraphMemoryService - 图记忆服务层
* 封装 GraphDatabase提供 AI Agent 友好的图记忆操作方法
* 参考main 分支 core/tools/memory_tools.py
*/
import { GraphDatabase } from '../model/GraphDatabase';
// ========= 接口定义 =========
export interface EntityInfo {
name: string;
type: string;
mentionCount: number;
depth?: number;
}
export interface RelationInfo {
source: string;
target: string;
type: string;
confidence: number;
sessionId?: string;
turnId?: number;
depth?: number;
}
export interface TripletInput {
subject: string;
relation: string;
object: string;
confidence?: number;
}
export interface CleanupResult {
cleaned: number;
deletedRelations?: number;
deletedOrphans?: number;
dryRun?: boolean;
message: string;
}
export interface SearchResult {
name: string;
type: string;
mentions: number;
}
export interface TaskInfo {
taskId: string;
description: string;
state: string;
infoCount: number;
updatedAt: string;
}
export interface NodeData {
id: number;
label: string;
type: string;
mentions: number;
}
export interface EdgeData {
from: number;
to: number;
label: string;
weight: number;
}
export interface GraphData {
nodes: NodeData[];
edges: EdgeData[];
}
// 内部接口 — 用于替换内联对象类型声明
export interface TimeRangeParams {
days: number;
}
export interface PurgeCriteriaParams {
subjectContains?: string;
relationType?: string;
targetContains?: string;
sessionId?: string;
}
export interface NewRelationParams {
relation: string;
target: string;
}
interface TripletData {
subject: string;
relation: string;
object: string;
}
interface CriteriaData {
subject?: string;
target?: string;
relation?: string;
sessionId?: string;
}
export interface MemoryRecallResult {
entities: EntityInfo[];
relations: RelationInfo[];
message: string;
}
export interface MemoryRecallParams {
queryIntent: string;
seedEntities?: string[];
depth?: number;
timeRange?: TimeRangeParams;
sessionFilter?: string;
}
export interface MemoryCommitParams {
triplets: TripletInput[];
entityTypes?: Record<string, string>;
sessionId?: string;
turnId?: number;
}
interface MemoryCommitResult {
committedCount: number;
details: string[];
}
export interface MemoryPurgeParams {
criteria: PurgeCriteriaParams;
mode: 'soft' | 'hard' | 'supersede';
newRelation?: NewRelationParams;
}
interface MemoryPurgeResult {
deletedCount: number;
message: string;
}
interface HotNodeInfo {
name: string;
mentionCount: number;
type: string;
}
interface MemoryIntrospectResult {
entityCount: number;
relationCount: number;
hotNodes: HotNodeInfo[];
message: string;
}
interface ArchiveResult {
archived: number;
message: string;
}
interface PersonaResult {
success: boolean;
message: string;
}
export interface PersonaQueryResult {
persona: Record<string, string>;
found: boolean;
}
interface TaskCreateResult {
success: boolean;
taskId: string;
message: string;
}
interface TaskActionResult {
success: boolean;
message: string;
}
export interface TaskQueryResult {
tasks: TaskInfo[];
message: string;
}
export interface PersonaUpdateParams {
tone?: string;
style?: string;
personality?: string;
catchphrase?: string;
background?: string;
}
export interface TaskCreateParams {
taskId: string;
description: string;
infoNodes?: string[];
}
export interface TaskSetStateParams {
taskId: string;
state: '进行中' | '已完成' | '已暂停' | '已取消';
}
export interface TaskDeleteParams {
taskId: string;
deleteInfoNodes?: boolean;
}
export interface TaskLinkInfoParams {
taskId: string;
infoNodeNames: string[];
}
export interface TaskArchiveParams {
taskId: string;
summary?: string;
}
export interface TaskQueryParams {
limit?: number;
stateFilter?: string;
}
// 人设节点的固定名称
const PERSONA_NODE_NAME: string = 'trulymem_persona_identity';
const PERSONA_NODE_TYPE: string = 'PersonaNode';
// ========= GraphMemoryService =========
export class GraphMemoryService {
private db: GraphDatabase;
constructor(db: GraphDatabase) {
this.db = db;
}
// ========= 记忆操作 =========
/**
* 记忆召回 — 关键词搜索 + BFS 扩展
* 对应 tools.memory_recall
*/
async memoryRecall(params: MemoryRecallParams): Promise<MemoryRecallResult> {
const depth: number = params.depth ?? 2;
const result = await this.db.recall(
params.queryIntent,
params.seedEntities,
depth,
params.sessionFilter
);
const entities: EntityInfo[] = result.entities.map(e => {
const entityItem: EntityInfo = {
name: e.name,
type: e.type,
mentionCount: e.mention_count,
depth: e.depth ?? 0
};
return entityItem;
});
const relations: RelationInfo[] = result.relations.map(r => {
const relationItem: RelationInfo = {
source: r.source,
target: r.target,
type: r.type,
confidence: r.confidence,
sessionId: r.session_id,
turnId: r.turn_id,
depth: r.depth ?? 0
};
return relationItem;
});
return { entities, relations, message: result.message };
}
/**
* 记忆写入 — 批量三元组
* 对应 tools.memory_commit
*/
async memoryCommit(params: MemoryCommitParams): Promise<MemoryCommitResult> {
const details: string[] = [];
for (const t of params.triplets) {
const subject = t.subject.trim();
const relation = t.relation.trim();
const object = t.object.trim();
if (!subject || !relation || !object) {
continue;
}
const triplets: TripletData[] = [{ subject, relation, object }];
await this.db.commit(
triplets,
params.entityTypes,
params.sessionId,
params.turnId
);
details.push(`${subject} -[${relation}]-> ${object}`);
}
const result: MemoryCommitResult = {
committedCount: details.length,
details
};
return result;
}
/**
* 记忆删除
* 对应 tools.memory_purge
*/
async memoryPurge(params: MemoryPurgeParams): Promise<MemoryPurgeResult> {
// soft 模式:调用 db.purge 逻辑删除
if (params.mode === 'supersede' && params.newRelation) {
// supersede: 先软删除旧关系,再新建
const softSubject = params.criteria.subjectContains;
const softRelation = params.criteria.relationType;
const softTarget = params.criteria.targetContains;
await this.db.purge({
subject: softSubject,
relation: softRelation,
target: softTarget
}, 'soft');
// 新建替代关系
const newSubj = softSubject || '';
if (newSubj) {
await this.db.commit([{
subject: newSubj,
relation: params.newRelation.relation,
object: params.newRelation.target
}]);
}
const supersedeResult: MemoryPurgeResult = {
deletedCount: 1,
message: '已用 supersede 模式替代记忆'
};
return supersedeResult;
}
const subjContains = params.criteria.subjectContains;
const relType = params.criteria.relationType;
const tgtContains = params.criteria.targetContains;
const sessId = params.criteria.sessionId;
await this.db.purge({
subject: subjContains,
relation: relType,
target: tgtContains,
sessionId: sessId
}, params.mode === 'hard' ? 'hard' : 'soft');
const purgeResult: MemoryPurgeResult = {
deletedCount: subjContains || tgtContains || relType || sessId ? 1 : 0,
message: `已${params.mode === 'hard' ? '物理删除' : '软删除'}匹配的记忆`
};
return purgeResult;
}
/**
* 记忆状态查询
*/
async memoryIntrospect(sessionId?: string): Promise<MemoryIntrospectResult> {
const stats = await this.db.introspect();
const hotNodes: HotNodeInfo[] = [];
// 从所有节点中获取前10个高频节点
const searchAll = await this.db.search('');
const sorted = searchAll.sort((a, b) => b.mentions - a.mentions).slice(0, 10);
for (const n of sorted) {
const nodeInfo: HotNodeInfo = {
name: n.name,
mentionCount: n.mentions,
type: n.type
};
hotNodes.push(nodeInfo);
}
const result: MemoryIntrospectResult = {
entityCount: stats.entity_count,
relationCount: stats.relation_count,
hotNodes,
message: stats.message
};
return result;
}
/**
* 关键词搜索节点
*/
async search(keyword: string): Promise<SearchResult[]> {
return await this.db.search(keyword);
}
/**
* 归档旧记忆
*/
async archive(days: number): Promise<ArchiveResult> {
return await this.db.archive(days);
}
/**
* 清理已删除的记忆
*/
async cleanup(dryRun: boolean): Promise<CleanupResult> {
const result = await this.db.cleanup(dryRun);
const cleanupResult: CleanupResult = {
cleaned: result.cleaned,
deletedRelations: result.deleted_relations,
deletedOrphans: result.deleted_orphans,
dryRun: result.dry_run,
message: result.message || ''
};
return cleanupResult;
}
// ========= 人设管理 =========
/**
* 更新人设
* 对应 tools.persona_update
* 使用 PersonaNode + HAS_PERSONA 关系存储属性
*/
async personaUpdate(params: PersonaUpdateParams): Promise<PersonaResult> {
try {
// 1. 确保 PersonaNode 存在
const personaTriplets: TripletData[] = [];
const entityTypes: Record<string, string> = {};
entityTypes[PERSONA_NODE_NAME] = PERSONA_NODE_TYPE;
// 2. 逐个属性写入(作为关系),不使用 as any
const toneVal = params.tone;
const styleVal = params.style;
const personalityVal = params.personality;
const catchphraseVal = params.catchphrase;
const backgroundVal = params.background;
if (toneVal) {
personaTriplets.push({
subject: PERSONA_NODE_NAME,
relation: 'HAS_PERSONA_TONE',
object: toneVal
});
}
if (styleVal) {
personaTriplets.push({
subject: PERSONA_NODE_NAME,
relation: 'HAS_PERSONA_STYLE',
object: styleVal
});
}
if (personalityVal) {
personaTriplets.push({
subject: PERSONA_NODE_NAME,
relation: 'HAS_PERSONA_PERSONALITY',
object: personalityVal
});
}
if (catchphraseVal) {
personaTriplets.push({
subject: PERSONA_NODE_NAME,
relation: 'HAS_PERSONA_CATCHPHRASE',
object: catchphraseVal
});
}
if (backgroundVal) {
personaTriplets.push({
subject: PERSONA_NODE_NAME,
relation: 'HAS_PERSONA_BACKGROUND',
object: backgroundVal
});
}
if (personaTriplets.length > 0) {
await this.db.commit(personaTriplets, entityTypes);
}
const successResult: PersonaResult = {
success: true,
message: `已更新 ${personaTriplets.length} 个人设属性`
};
return successResult;
} catch (e) {
const errorResult: PersonaResult = {
success: false,
message: `更新人设失败: ${(e as Error).message || ''}`
};
return errorResult;
}
}
/**
* 清除人设
* 对应 tools.persona_clear
*/
async personaClear(): Promise<PersonaResult> {
try {
await this.db.purge({ subject: PERSONA_NODE_NAME }, 'hard');
const result: PersonaResult = { success: true, message: '已清除所有人设信息' };
return result;
} catch (e) {
const errorResult: PersonaResult = {
success: false,
message: `清除人设失败: ${(e as Error).message || ''}`
};
return errorResult;
}
}
/**
* 查询当前人设
*/
async personaQuery(): Promise<PersonaQueryResult> {
const result = await this.db.recall(PERSONA_NODE_NAME, [PERSONA_NODE_NAME], 2);
const persona: Record<string, string> = {};
for (const rel of result.relations) {
if (rel.source === PERSONA_NODE_NAME && rel.type.startsWith('HAS_PERSONA_')) {
const key = rel.type.replace('HAS_PERSONA_', '').toLowerCase();
persona[key] = rel.target;
}
}
const queryResult: PersonaQueryResult = {
persona,
found: Object.keys(persona).length > 0
};
return queryResult;
}
// ========= 任务管理 =========
/**
* 创建任务节点
* 对应 tools.task_create
*/
async taskCreate(params: TaskCreateParams): Promise<TaskCreateResult> {
try {
const entityTypes: Record<string, string> = {};
entityTypes[params.taskId] = 'TaskNode';
const triplets: TripletData[] = [
{ subject: params.taskId, relation: 'description', object: params.description },
{ subject: params.taskId, relation: 'has_state', object: '进行中' }
];
if (params.infoNodes && params.infoNodes.length > 0) {
for (const infoNode of params.infoNodes) {
entityTypes[infoNode] = 'InfoNode';
triplets.push({ subject: params.taskId, relation: 'CONTAINS_INFO', object: infoNode });
}
}
await this.db.commit(triplets, entityTypes);
const result: TaskCreateResult = {
success: true,
taskId: params.taskId,
message: `已创建任务: ${params.taskId}`
};
return result;
} catch (e) {
const errorResult: TaskCreateResult = {
success: false,
taskId: params.taskId,
message: `创建任务失败: ${(e as Error).message || ''}`
};
return errorResult;
}
}
/**
* 设置任务状态
* 对应 tools.task_set_state
*/
async taskSetState(params: TaskSetStateParams): Promise<TaskActionResult> {
try {
// 先删旧的 has_state 关系,再新建
await this.db.purge({ subject: params.taskId, relation: 'has_state' }, 'soft');
await this.db.commit([{ subject: params.taskId, relation: 'has_state', object: params.state }]);
const result: TaskActionResult = {
success: true,
message: `任务 ${params.taskId} 状态已设为: ${params.state}`
};
return result;
} catch (e) {
const errorResult: TaskActionResult = {
success: false,
message: `设置任务状态失败: ${(e as Error).message || ''}`
};
return errorResult;
}
}
/**
* 删除任务
* 对应 tools.task_delete
*/
async taskDelete(params: TaskDeleteParams): Promise<TaskActionResult> {
try {
// 删除所有关联关系
await this.db.purge({ subject: params.taskId }, 'hard');
if (params.deleteInfoNodes !== false) {
await this.db.purge({ target: params.taskId }, 'hard');
}
const result: TaskActionResult = { success: true, message: `已删除任务: ${params.taskId}` };
return result;
} catch (e) {
const errorResult: TaskActionResult = {
success: false,
message: `删除任务失败: ${(e as Error).message || ''}`
};
return errorResult;
}
}
/**
* 关联信息节点到任务
* 对应 tools.task_link_info
*/
async taskLinkInfo(params: TaskLinkInfoParams): Promise<TaskActionResult> {
try {
const triplets: TripletData[] = [];
const entityTypes: Record<string, string> = {};
for (const nodeName of params.infoNodeNames) {
entityTypes[nodeName] = 'InfoNode';
triplets.push({ subject: params.taskId, relation: 'CONTAINS_INFO', object: nodeName });
}
await this.db.commit(triplets, entityTypes);
const result: TaskActionResult = { success: true, message: `已关联 ${triplets.length} 个信息节点` };
return result;
} catch (e) {
const errorResult: TaskActionResult = {
success: false,
message: `关联信息节点失败: ${(e as Error).message || ''}`
};
return errorResult;
}
}
/**
* 归档任务
* 对应 tools.task_archive
*/
async taskArchive(params: TaskArchiveParams): Promise<TaskActionResult> {
try {
await this.taskSetState({ taskId: params.taskId, state: '已暂停' });
if (params.summary) {
await this.db.commit([
{ subject: params.taskId, relation: 'archive_summary', object: params.summary }
]);
}
const result: TaskActionResult = { success: true, message: `已归档任务: ${params.taskId}` };
return result;
} catch (e) {
const errorResult: TaskActionResult = {
success: false,
message: `归档任务失败: ${(e as Error).message || ''}`
};
return errorResult;
}
}
/**
* 查询任务列表
* 对应 tools.task_query
*/
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} 个任务`
};
return queryResult;
}
// ========= 图数据 =========
/**
* 获取用于 WebView 的完整图数据
*/
async getGraphDataForView(): Promise<GraphData> {
// 用空关键词召回所有数据
const recallResult = await this.db.recall('', [], 3);
const nodes: NodeData[] = [];
const edges: EdgeData[] = [];
let nodeIdCounter = 1;
const nameToId: Record<string, number> = {};
for (const entity of recallResult.entities) {
const id = nodeIdCounter;
nodeIdCounter++;
nameToId[entity.name] = id;
const node: NodeData = {
id,
label: entity.name,
type: entity.type,
mentions: entity.mention_count
};
nodes.push(node);
}
for (const rel of recallResult.relations) {
const from = nameToId[rel.source];
const to = nameToId[rel.target];
if (from !== undefined && to !== undefined) {
const edge: EdgeData = {
from,
to,
label: rel.type,
weight: rel.confidence
};
edges.push(edge);
}
}
const graphData: GraphData = { nodes, edges };
return graphData;
}
}

View File

@ -89,7 +89,12 @@
loadGraphData();
}
});
window.parent.postMessage('request_graph_data', '*');
// 通知 ArkTS 请求图数据
try {
if (window.nativeBridge && window.nativeBridge.requestGraphData) {
window.nativeBridge.requestGraphData();
}
} catch(e) {}
animate();
}
@ -265,7 +270,16 @@
if (intersects.length > 0) {
const node = intersects[0].object;
if (selectedNode === node) { selectedNode = null; document.getElementById('node-info').style.display = 'none'; }
else { selectedNode = node; showNodeInfo(node.userData.nodeData, true); }
else {
selectedNode = node;
showNodeInfo(node.userData.nodeData, true);
// 通知 ArkTS 节点被点击
try {
if (window.nativeBridge && window.nativeBridge.onNodeClick) {
window.nativeBridge.onNodeClick(node.userData.nodeData.id, node.userData.nodeData.name);
}
} catch(e) {}
}
}
}

View File

@ -2,6 +2,10 @@
"modelVersion": "5.0.5",
"description": "Please describe the basic information.",
"dependencies": {
"@kit.UIDesignKit": "^6.1.0",
"@kit.ArkUI": "^6.1.0",
"@kit.AbilityKit": "^6.1.0",
"@kit.BasicServicesKit": "^6.1.0"
},
"devDependencies": {
"@ohos/hypium": "1.0.24",