diff --git a/build-profile.json5 b/build-profile.json5 index 333ab15..a7b41fd 100644 --- a/build-profile.json5 +++ b/build-profile.json5 @@ -24,13 +24,87 @@ ] }, "modules": [ + { + "name": "common", + "srcPath": "./common", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "commonbusiness", + "srcPath": "./features/commonbusiness", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "graph", + "srcPath": "./features/graph", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "chat", + "srcPath": "./features/chat", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "settings", + "srcPath": "./features/settings", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "phone", + "srcPath": "./products/phone", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, { "name": "entry", "srcPath": "./entry", "targets": [ { "name": "default", - "applyToProducts": ["default"] + "applyToProducts": [ + "default" + ] } ] }, @@ -39,7 +113,10 @@ "srcPath": "./trulymem-core", "targets": [ { - "name": "default" + "name": "default", + "applyToProducts": [ + "default" + ] } ] } diff --git a/common/Index.ets b/common/Index.ets new file mode 100644 index 0000000..a5c974f --- /dev/null +++ b/common/Index.ets @@ -0,0 +1,5 @@ +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 { ImmersiveTabNavigation } from './src/main/ets/component/ImmersiveTabNavigation'; diff --git a/common/build-profile.json5 b/common/build-profile.json5 new file mode 100644 index 0000000..b823c6e --- /dev/null +++ b/common/build-profile.json5 @@ -0,0 +1,8 @@ +{ + "apiType": "stageMode", + "targets": [ + { + "name": "default" + } + ] +} diff --git a/common/oh-package.json5 b/common/oh-package.json5 new file mode 100644 index 0000000..ca4b633 --- /dev/null +++ b/common/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "name": "@ohos/common", + "version": "1.0.0", + "description": "TrulyMEM common module", + "main": "Index.ets", + "author": "", + "license": "", + "dependencies": {} +} diff --git a/common/src/main/ets/component/ImmersiveTabNavigation.ets b/common/src/main/ets/component/ImmersiveTabNavigation.ets new file mode 100644 index 0000000..739673e --- /dev/null +++ b/common/src/main/ets/component/ImmersiveTabNavigation.ets @@ -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('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') + } +} diff --git a/common/src/main/ets/constant/TrulyMEMConstants.ets b/common/src/main/ets/constant/TrulyMEMConstants.ets new file mode 100644 index 0000000..374db53 --- /dev/null +++ b/common/src/main/ets/constant/TrulyMEMConstants.ets @@ -0,0 +1,44 @@ +export class Constants { + static readonly DB_NAME: string = 'trulymem.db'; + static readonly CONFIG_PREF_NAME: string = 'trulymem_config'; + static readonly DEFAULT_BASE_URL: string = 'https://api.deepseek.com'; + static readonly DEFAULT_MODEL: string = 'deepseek-chat'; + static readonly SECURITY_LEVEL: number = 1; // S1 + + // Table names + static readonly TABLE_NODES: string = 'nodes'; + static readonly TABLE_RELATIONS: string = 'relations'; + static readonly TABLE_CHAT: string = 'chat_records'; + + // SQL definitions + static readonly SQL_CREATE_NODES: string = ` + 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 DEFAULT (datetime('now','localtime')), + updated_at TEXT DEFAULT (datetime('now','localtime')) + )`; + + static readonly SQL_CREATE_RELATIONS: string = ` + 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, + created_at TEXT DEFAULT (datetime('now','localtime')), + FOREIGN KEY (subject_id) REFERENCES nodes(id) ON DELETE CASCADE, + FOREIGN KEY (object_id) REFERENCES nodes(id) ON DELETE CASCADE + )`; + + static readonly SQL_CREATE_CHAT: string = ` + CREATE TABLE IF NOT EXISTS chat_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + role TEXT NOT NULL, + content TEXT NOT NULL, + tools TEXT, + created_at TEXT DEFAULT (datetime('now','localtime')) + )`; +} diff --git a/common/src/main/ets/model/GraphDatabase.ets b/common/src/main/ets/model/GraphDatabase.ets new file mode 100644 index 0000000..58e96e3 --- /dev/null +++ b/common/src/main/ets/model/GraphDatabase.ets @@ -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 { + this.context = context; + this.store = await relationalStore.getRdbStore(context, STORE_CONFIG); + await this.createTables(); + } + + private async createTables(): Promise { + 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, sessionId?: string, turnId?: number): Promise { + 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 { + 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 { + 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 { + 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(); + let seedEntityIds = new Set(); + + 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(entityIds); + const visitedEntityIds = new Set(entityIds); + // 批量预加载所有相关节点名称,减少 N+1 查询 + const nodeNameCache = new Map(); + + 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(); + + 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 { + 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 { + if (!this.store || nodeIds.length === 0) return []; + const relations: RelationQueryResult[] = []; + // 批量预加载所有节点名称到缓存,避免 N+1 查询 + const nodeNameCache = new Map(); + 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 { + 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 { + 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 { + 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(); + 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 { + 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(); + 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 { + 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 { + 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(); + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + if (!this.store) return; + const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records'); + await this.store.delete(predicates); + } + + private async getAllNodes(): Promise { + 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 { + 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; + } +} \ No newline at end of file diff --git a/common/src/main/ets/service/AIAgentService.ets b/common/src/main/ets/service/AIAgentService.ets new file mode 100644 index 0000000..030a9a5 --- /dev/null +++ b/common/src/main/ets/service/AIAgentService.ets @@ -0,0 +1,880 @@ +/** + * AIAgentService - AI Agent 服务层 + * 管理上下文感知的 AI 对话,注入图数据作为上下文, + * 解析 AI 返回中的记忆操作,调用 GraphMemoryService 执行 + * 参考:main 分支 core/graph_client.py + */ +import http from '@ohos.net.http'; +import dataPreferences from '@ohos.data.preferences'; +import { Context } from '@ohos.abilityAccessCtrl'; +import { GraphMemoryService, EntityInfo, RelationInfo, TaskInfo, MemoryRecallParams, MemoryCommitParams, MemoryPurgeParams, PurgeCriteriaParams, NewRelationParams, PersonaUpdateParams, TaskCreateParams, TaskSetStateParams, TaskDeleteParams, TaskLinkInfoParams, TaskArchiveParams, TaskQueryParams, TripletInput, PersonaQueryResult, TaskQueryResult, MemoryRecallResult } from './GraphMemoryService'; +import { TimeRangeParams } from '../model/GraphDatabase'; + +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) +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; + dryRun?: ToolParamProperty; + keyword?: ToolParamProperty; + attribute?: 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; + 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 `你是 TrulyMEM(True 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?): 删除/修正记忆 +- memory_introspect(sessionId?): 查看记忆状态统计 +- memory_archive(days?): 归档旧记忆 +- memory_cleanup(dryRun?): 清理已删除数据 +- memory_query_archived(days?, keyword?): 查询已归档记忆 +- context_rewrite(summary): 压缩工具调用上下文 +- persona_update(tone?, style?, personality?, catchphrase?, background?): 更新人设 +- persona_remove(attribute): 删除单条人设属性 +- 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?): 查询任务列表 + +## 工具调用规则 +1. 每轮对话必须按顺序执行:步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求 +2. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调! +3. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文 + +## 写入规则 +用户明确表达以下信息时必须写入记忆: +- 偏好、兴趣 +- 个人信息(工作、项目、学习) +- 计划安排 +- 当前状态 +- 结论性事实 + +推理得到的信息可以写入但需标注 [推测]。`; +} + +// ========= 工具定义 ========= + +// 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 introspectProps: ToolPropertiesDefinition = { + sessionId: makeStringProp('会话ID(可选)') +}; +const archiveProps2: ToolPropertiesDefinition = { + days: makeIntegerProp('归档天数,默认30') +}; +const cleanupProps: ToolPropertiesDefinition = { + dryRun: makeBoolProp('仅预览不删除') +}; +const queryArchivedProps: ToolPropertiesDefinition = { + days: makeIntegerProp('最近N天内的归档记录'), + keyword: makeStringProp('关键词过滤') +}; +const contextRewriteProps: ToolPropertiesDefinition = { + summary: makeStringProp('压缩后的摘要文本,必须包含工具调用元信息') +}; +const personaRemoveProps: ToolPropertiesDefinition = { + attribute: makeStringProp('要删除的属性名(如:扮演角色、说话风格)') +}; + +const TOOLS_DEFINITION: ToolFunctionDef[] = [ + makeToolDef('memory_recall', '检索记忆。支持关键词、种子实体、深度扩展。返回相关实体和关系。', recallProps, ['queryIntent']), + makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库。', commitProps, ['triplets']), + makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。', 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('persona_update', '更新AI人设属性(语气、风格、性格等)。', personaProps), + makeToolDef('persona_remove', '删除单条人设属性。保留其他人设不变。', personaRemoveProps, ['attribute']), + 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' + | 'memoryIntrospect' + | 'memoryArchive' + | 'memoryCleanup' + | 'memoryQueryArchived' + | 'contextRewrite' + | 'personaUpdate' + | 'personaRemove' + | 'personaClear' + | 'taskCreate' + | 'taskSetState' + | 'taskDelete' + | 'taskLinkInfo' + | 'taskArchive' + | 'taskQuery'; + +const TOOL_HANDLER_MAP: Record = { + 'memory_recall': 'memoryRecal', + 'memory_commit': 'memoryCommit', + 'memory_purge': 'memoryPurge', + 'memory_introspect': 'memoryIntrospect', + 'memory_archive': 'memoryArchive', + 'memory_cleanup': 'memoryCleanup', + 'memory_query_archived': 'memoryQueryArchived', + 'context_rewrite': 'contextRewrite', + 'persona_update': 'personaUpdate', + 'persona_remove': 'personaRemove', + '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; + + private appContext: Context; + + constructor(memoryService: GraphMemoryService, appContext: Context, sessionId?: string) { + this.memoryService = memoryService; + this.appContext = appContext; + 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 { + 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 = this.appContext; + 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 = 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 { + 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): Promise { + 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, + 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 'memoryIntrospect': { + const introspectResult = await this.memoryService.memoryIntrospect(args.sessionId as string); + const result: ToolCallResult = { + name: 'memory_introspect', + success: true, + message: `实体: ${introspectResult.entityCount}, 关系: ${introspectResult.relationCount}, 热点: ${introspectResult.hotNodes.length}` + }; + return result; + } + + case 'memoryArchive': { + const archiveResult = await this.memoryService.archive(args.days as number); + const result: ToolCallResult = { + name: 'memory_archive', + success: true, + message: `已归档 ${archiveResult.archived} 条关系` + }; + return result; + } + + case 'memoryCleanup': { + const cleanupResult = await this.memoryService.cleanup((args.dryRun as boolean) !== false); + const result: ToolCallResult = { + name: 'memory_cleanup', + success: true, + message: `清理: ${cleanupResult.cleaned} 条关系, ${cleanupResult.deletedOrphans} 个孤儿节点` + (cleanupResult.dryRun ? ' (预览模式)' : '') + }; + return result; + } + + case 'memoryQueryArchived': { + const qaResult = await this.memoryService.queryArchived(args.days as number, args.keyword as string); + const result: ToolCallResult = { + name: 'memory_query_archived', + success: true, + message: `找到 ${qaResult.length} 条归档记录` + }; + return result; + } + + case 'contextRewrite': { + const summary = args.summary as string; + const result: ToolCallResult = { + name: 'context_rewrite', + success: summary.includes('[工具调用总结'), + message: summary.includes('[工具调用总结') ? '上下文已压缩' : '格式错误:必须包含[工具调用总结]标记' + }; + 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 'personaRemove': { + const prResult = await this.memoryService.personaRemove(args.attribute as string); + const result: ToolCallResult = { + name: 'persona_remove', + success: prResult.success, + message: prResult.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 { + 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') : '【新对话】'; + } +} diff --git a/common/src/main/ets/service/GraphMemoryService.ets b/common/src/main/ets/service/GraphMemoryService.ets new file mode 100644 index 0000000..a74323d --- /dev/null +++ b/common/src/main/ets/service/GraphMemoryService.ets @@ -0,0 +1,1067 @@ +/** + * GraphMemoryService - 图记忆服务层 + * 封装 GraphDatabase,提供 AI Agent 友好的图记忆操作方法 + * 参考:main 分支 core/tools/memory_tools.py + */ +import { GraphDatabase, RelationQueryResult, TimeRangeParams } from '../model/GraphDatabase'; + +// ========= 接口定义 ========= + +export interface SnapshotData { + entities: EntityInfo[]; + relations: RelationInfo[]; +} + +export interface GraphDataNode { + id: number; + label: string; + type: string; + mentions: number; + depth?: number; +} + +export interface GraphDataEdge { + from: number; + to: number; + label: string; + weight: number; + depth?: number; + sessionId?: string; + turnId?: number; +} + +export interface SnapshotEntity { + name: string; + type: string; + mention_count: number; + depth?: number; +} + +export interface SnapshotRelation { + source: string; + target: string; + type: string; + confidence: number; + session_id?: string; + turn_id?: number; + depth?: number; +} + +export interface GraphOutput { + nodes: GraphDataNode[]; + edges: GraphDataEdge[]; +} + +export interface SnapshotOutput { + entities: SnapshotEntity[]; + relations: SnapshotRelation[]; +} + +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 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; + 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; + 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; +} + +// 节点详情连接项接口 +export interface ConnectionItem { + type: string; + target_name: string; +} + +// 节点详情返回接口 +export interface NodeDetailInfo { + name: string; + type: string; + mention_count: number; + connection_count: number; + connections: ConnectionItem[]; +} + +// 图数据统计接口 +export interface GraphStats { + maxDegree: number; + avgDegree: number; +} + +// 人设节点的固定名称 +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 { + const depth: number = params.depth ?? 2; + const result = await this.db.recall( + params.queryIntent, + params.seedEntities, + depth, + undefined, + 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 { + 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 { + // 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 { + 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 { + return await this.db.search(keyword); + } + + /** + * 归档旧记忆 + */ + async archive(days: number): Promise { + return await this.db.archive(days); + } + + /** + * 清理已删除的记忆 + */ + async cleanup(dryRun: boolean): Promise { + 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.memory_graph + */ + async memoryGraph(timeRange: TimeRangeParams, sessionFilter?: string): Promise { + const dbResult = await this.db.graph(timeRange, sessionFilter); + const nodeCount: number = dbResult.nodes.length; + const edgeCount: number = dbResult.edges.length; + const nodeList: GraphDataNode[] = []; + const edgeList: GraphDataEdge[] = []; + let idx: number = 0; + while (idx < nodeCount) { + const n = dbResult.nodes[idx]; + const id1: number = n.id; + const label1: string = n.label; + const type1: string = n.type; + const mentions1: number = n.mentions; + const depth1: number | undefined = n.depth; + const graphNode: GraphDataNode = { + id: id1, + label: label1, + type: type1, + mentions: mentions1, + depth: depth1 + }; + nodeList.push(graphNode); + idx++; + } + idx = 0; + while (idx < edgeCount) { + const e = dbResult.edges[idx]; + const from1: number = e.from; + const to1: number = e.to; + const label1: string = e.label; + const weight1: number = e.weight; + const depth1: number | undefined = e.depth; + const sessionId1: string | undefined = e.sessionId; + const turnId1: number | undefined = e.turnId; + const graphEdge: GraphDataEdge = { + from: from1, + to: to1, + label: label1, + weight: weight1, + depth: depth1, + sessionId: sessionId1, + turnId: turnId1 + }; + edgeList.push(graphEdge); + idx++; + } + const out: GraphOutput = { + nodes: nodeList, + edges: edgeList + }; + return out; + } + + /** + * 记忆快照 — 在指定时间范围内查询实体和关系 + * 对应 tools.memory_snapshot + */ + async memorySnapshot(timeRange: TimeRangeParams, sessionFilter?: string): Promise { + const dbResult = await this.db.snapshot(timeRange, sessionFilter); + const entityCount: number = dbResult.entities.length; + const relationCount: number = dbResult.relations.length; + const entityList: SnapshotEntity[] = []; + const relationList: SnapshotRelation[] = []; + let idx: number = 0; + while (idx < entityCount) { + const e = dbResult.entities[idx]; + const name1: string = e.name; + const type1: string = e.type; + const mentionCount1: number = e.mention_count; + const depth1: number | undefined = e.depth; + const entity: SnapshotEntity = { + name: name1, + type: type1, + mention_count: mentionCount1, + depth: depth1 + }; + entityList.push(entity); + idx++; + } + idx = 0; + while (idx < relationCount) { + const r = dbResult.relations[idx]; + const source1: string = r.source; + const target1: string = r.target; + const type1: string = r.type; + const confidence1: number = r.confidence; + const sessionId1: string | undefined = r.session_id; + const turnId1: number | undefined = r.turn_id; + const depth1: number | undefined = r.depth; + const relation: SnapshotRelation = { + source: source1, + target: target1, + type: type1, + confidence: confidence1, + session_id: sessionId1, + turn_id: turnId1, + depth: depth1 + }; + relationList.push(relation); + idx++; + } + const out2: SnapshotOutput = { + entities: entityList, + relations: relationList + }; + return out2; + } + + /** + * 记忆清理 — 归档旧记忆并清理孤立节点 + * 对应 tools.memory_cleanup + */ + async memoryCleanup(dryRun: boolean = false): Promise { + 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.memory_purge + */ + + + /** + * 查询已归档的记忆 + * 对应 tools.memory_query_archived + */ + async queryArchived(days?: number, keyword?: string): Promise { + try { + const result = await this.db.queryArchived(days, keyword); + return result; + } catch (e) { + console.error('queryArchived error: ' + JSON.stringify(e)); + return []; + } + } + + // ========= 人设管理 ========= + + /** + * 更新人设 + * 对应 tools.persona_update + * 使用 PersonaNode + HAS_PERSONA 关系存储属性 + */ + async personaUpdate(params: PersonaUpdateParams): Promise { + try { + // 1. 确保 PersonaNode 存在 + const personaTriplets: TripletData[] = []; + const entityTypes: Record = {}; + 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 { + 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; + } + } + + /** + * 删除单条人设属性 + * 对应 tools.persona_remove + */ + async personaRemove(attribute: string): Promise { + try { + // 将 attribute 转为关系名格式 + const relationName = 'HAS_PERSONA_' + attribute.toUpperCase(); + await this.db.purge({ subject: PERSONA_NODE_NAME, relation: relationName }, 'hard'); + return { + success: true, + message: `已删除人设属性: ${attribute}` + }; + } catch (e) { + return { + success: false, + message: `删除人设属性失败: ${(e as Error).message || ''}` + }; + } + } + + /** + * 查询当前人设 + */ + async personaQuery(): Promise { + const result = await this.db.recall(PERSONA_NODE_NAME, [PERSONA_NODE_NAME], 2); + const persona: Record = {}; + 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 { + try { + const entityTypes: Record = {}; + 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 { + 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 { + 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 { + try { + const triplets: TripletData[] = []; + const entityTypes: Record = {}; + 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 { + 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 { + 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 { + // 用空关键词召回所有数据 + const recallResult = await this.db.recall('', [], 3); + const nodes: NodeData[] = []; + const edges: EdgeData[] = []; + let nodeIdCounter = 1; + const nameToId: Record = {}; + + 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; + } + + /** + * 查询节点的完整信息 — 自身属性 + 所有相连关系 + */ + async getNodeDetail(nodeName: string): Promise { + try { + const recallResult = await this.db.recall(nodeName, [nodeName], 1); + if (recallResult.entities.length === 0) { + return null; + } + + const entity = recallResult.entities[0]; + const connections: ConnectionItem[] = []; + let connectionCount = 0; + + for (const rel of recallResult.relations) { + if (rel.source === nodeName) { + const conn: ConnectionItem = { + type: rel.type, + target_name: rel.target + }; + connections.push(conn); + connectionCount++; + } else if (rel.target === nodeName) { + const conn: ConnectionItem = { + type: rel.type + ' (反向)', + target_name: rel.source + }; + connections.push(conn); + connectionCount++; + } + } + + const detail: NodeDetailInfo = { + name: entity.name, + type: entity.type, + mention_count: entity.mention_count, + connection_count: connectionCount, + connections + }; + return detail; + } catch (err) { + console.error('getNodeDetail error: ' + JSON.stringify(err)); + return null; + } + } + + /** + * 返回带连接度数的图数据(每个节点增加 degree 字段) + */ + async getJoinedData(): Promise { + const graphData = await this.getGraphDataForView(); + + // 计算每个节点的连接度数 + const degreeMap: Record = {}; + for (const edge of graphData.edges) { + degreeMap[edge.from] = (degreeMap[edge.from] || 0) + 1; + degreeMap[edge.to] = (degreeMap[edge.to] || 0) + 1; + } + + // 手动为节点附加 degree(ArkTS 不支持展开运算符) + const nodesWithDegree: NodeData[] = []; + for (let i = 0; i < graphData.nodes.length; i++) { + const orig = graphData.nodes[i]; + const copy: NodeData = { + id: orig.id, + label: orig.label, + type: orig.type, + mentions: orig.mentions + }; + nodesWithDegree.push(copy); + } + + const graphResult: GraphData = { + nodes: nodesWithDegree, + edges: graphData.edges + }; + return graphResult; + } +} diff --git a/common/src/main/module.json5 b/common/src/main/module.json5 new file mode 100644 index 0000000..08343c6 --- /dev/null +++ b/common/src/main/module.json5 @@ -0,0 +1,13 @@ +{ + "module": { + "name": "common", + "type": "shared", + "description": "TrulyMEM common shared module", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true + } +} diff --git a/features/chat/Index.ets b/features/chat/Index.ets new file mode 100644 index 0000000..38218c6 --- /dev/null +++ b/features/chat/Index.ets @@ -0,0 +1 @@ +export { ChatPage } from './src/main/ets/pages/ChatPage'; diff --git a/features/chat/build-profile.json5 b/features/chat/build-profile.json5 new file mode 100644 index 0000000..b823c6e --- /dev/null +++ b/features/chat/build-profile.json5 @@ -0,0 +1,8 @@ +{ + "apiType": "stageMode", + "targets": [ + { + "name": "default" + } + ] +} diff --git a/features/chat/oh-package.json5 b/features/chat/oh-package.json5 new file mode 100644 index 0000000..0a5528b --- /dev/null +++ b/features/chat/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "@ohos/chat", + "version": "1.0.0", + "description": "TrulyMEM chat feature module", + "main": "Index.ets", + "author": "", + "license": "", + "dependencies": { + "@ohos/common": "file:../../common" + } +} diff --git a/features/chat/src/main/ets/pages/ChatPage.ets b/features/chat/src/main/ets/pages/ChatPage.ets new file mode 100644 index 0000000..d2fb153 --- /dev/null +++ b/features/chat/src/main/ets/pages/ChatPage.ets @@ -0,0 +1,171 @@ +import { GraphDatabase } from '@ohos/common'; +import { GraphMemoryService } from '@ohos/common'; +import { AIAgentService, ChatMessage, AgentResponse } from '@ohos/common'; + +@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() { + // 初始化图记忆服务和 Agent + const memoryService = new GraphMemoryService(this.db); + this.agentService = new AIAgentService(memoryService, getContext(this)); + + // 加载历史消息(兼容旧数据:无 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() || !this.agentService) return; + + const userMessage: string = this.inputText; + this.inputText = ''; + + // 添加用户消息 + await this.db.saveChatMessage('user', userMessage, '', this.agentService.getSessionId()); + this.messages = [...this.messages, { role: 'user', content: userMessage }]; + + // 显示 loading + this.isThinking = true; + this.toolCallLog = ''; + + try { + // 通过 Agent 发送消息 + const agentResponse: AgentResponse = await this.agentService.sendMessage(userMessage); + + // 记录工具调用日志 + 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('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 === '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' ? '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)') + } +} diff --git a/features/commonbusiness/Index.ets b/features/commonbusiness/Index.ets new file mode 100644 index 0000000..90d8999 --- /dev/null +++ b/features/commonbusiness/Index.ets @@ -0,0 +1 @@ +// Common business module - placeholder for shared business logic diff --git a/features/commonbusiness/build-profile.json5 b/features/commonbusiness/build-profile.json5 new file mode 100644 index 0000000..b823c6e --- /dev/null +++ b/features/commonbusiness/build-profile.json5 @@ -0,0 +1,8 @@ +{ + "apiType": "stageMode", + "targets": [ + { + "name": "default" + } + ] +} diff --git a/features/commonbusiness/oh-package.json5 b/features/commonbusiness/oh-package.json5 new file mode 100644 index 0000000..d1d957d --- /dev/null +++ b/features/commonbusiness/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "@ohos/commonbusiness", + "version": "1.0.0", + "description": "TrulyMEM common business module", + "main": "Index.ets", + "author": "", + "license": "", + "dependencies": { + "@ohos/common": "file:../../common" + } +} diff --git a/features/graph/Index.ets b/features/graph/Index.ets new file mode 100644 index 0000000..7490455 --- /dev/null +++ b/features/graph/Index.ets @@ -0,0 +1 @@ +export { GraphPage } from './src/main/ets/pages/GraphPage'; diff --git a/features/graph/build-profile.json5 b/features/graph/build-profile.json5 new file mode 100644 index 0000000..b823c6e --- /dev/null +++ b/features/graph/build-profile.json5 @@ -0,0 +1,8 @@ +{ + "apiType": "stageMode", + "targets": [ + { + "name": "default" + } + ] +} diff --git a/features/graph/oh-package.json5 b/features/graph/oh-package.json5 new file mode 100644 index 0000000..6a5493b --- /dev/null +++ b/features/graph/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "@ohos/graph", + "version": "1.0.0", + "description": "TrulyMEM graph feature module", + "main": "Index.ets", + "author": "", + "license": "", + "dependencies": { + "@ohos/common": "file:../../common" + } +} diff --git a/features/graph/src/main/ets/pages/GraphPage.ets b/features/graph/src/main/ets/pages/GraphPage.ets new file mode 100644 index 0000000..fd3b3b9 --- /dev/null +++ b/features/graph/src/main/ets/pages/GraphPage.ets @@ -0,0 +1,313 @@ +/** + * GraphPage — 记忆星图页面 + * 使用 WebView 显示 Three.js 3D 图可视化 + * 通过 javaScriptProxy 与 WebView 双向通信 + */ +import web_webview from '@ohos.web.webview'; +import { GraphDatabase, RecallEntity } from '@ohos/common'; +import { GraphMemoryService, ConnectionItem, NodeDetailInfo } from '@ohos/common'; + +// ========= 图数据结构定义 ========= + +interface GraphNodeItem { + id: number; + label: string; + type: string; + mentions: number; +} + +interface GraphEdgeItem { + id: number; + source: number; + target: number; + label: string; + relation: string; +} + +// NodeDetailInfo 已从 GraphMemoryService 导入 + +// ========= WebView 原生桥接 ========= + +class NativeBridge { + private controller: web_webview.WebviewController; + private onRequestGraphData: () => void; + private onNodeClickCallback: (nodeId: number, nodeName: string) => void; + private onSearchCallback: (query: string) => void; + + constructor( + controller: web_webview.WebviewController, + onRequestGraphData: () => void, + onNodeClickCallback: (nodeId: number, nodeName: string) => void, + onSearchCallback: (query: string) => void + ) { + this.controller = controller; + this.onRequestGraphData = onRequestGraphData; + this.onNodeClickCallback = onNodeClickCallback; + this.onSearchCallback = onSearchCallback; + } + + onNodeClick(nodeId: number, nodeName: string): void { + console.info('Node clicked: id=' + nodeId + ', name=' + nodeName); + if (this.onNodeClickCallback) { + this.onNodeClickCallback(nodeId, nodeName); + } + } + + onSearch(query: string): void { + console.info('Search from WebView: ' + query); + if (this.onSearchCallback) { + this.onSearchCallback(query); + } + } + + 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; + @State selectedNodeDetail: NodeDetailInfo | null = null; + @State showNodeDetail: boolean = false; + @State searchText: string = ''; + private graphService: GraphMemoryService = new GraphMemoryService(this.db); + + // 初始化桥接对象 + private bridge: NativeBridge = new NativeBridge( + this.controller, + (): void => { this.pushGraphDataToWebView(); }, + (nodeId: number, nodeName: string): void => { this.handleNodeClick(nodeId, nodeName); }, + (query: string): void => { this.handleSearchFromWeb(query); } + ); + + /** + * 外部触发刷新图数据(聊天写入新记忆后调用) + */ + public async refreshGraphData(): Promise { + await this.pushGraphDataToWebView(); + } + + /** + * 处理节点点击 - 查询详细信息并显示浮层 + */ + private async handleNodeClick(nodeId: number, nodeName: string): Promise { + try { + // 从数据库查询节点详细信息 + const detail = await this.graphService.getNodeDetail(nodeName); + if (detail) { + this.selectedNodeDetail = detail; + this.showNodeDetail = true; + } + } catch (err) { + console.error('handleNodeClick error: ' + JSON.stringify(err)); + } + } + + /** + * 处理来自 WebView 的搜索请求 + */ + private handleSearchFromWeb(query: string): void { + this.searchText = query; + } + + /** + * 处理搜索输入 - 通知 WebView 过滤 + */ + private onSearchInput(value: string): void { + this.searchText = value; + const jsCode = `window.dispatchEvent(new MessageEvent('message', { data: { type: 'search_nodes', query: '${value}' } }));`; + this.controller.runJavaScript(jsCode); + } + + /** + * 关闭节点详情浮层 + */ + private closeNodeDetail(): void { + this.showNodeDetail = false; + this.selectedNodeDetail = null; + } + + /** + * 从数据库读取全量图数据,通过 runJavaScript 推送给 WebView + */ + private async pushGraphDataToWebView(): Promise { + 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 { + 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 { + const recallResult = await this.db.recall('', [], 3); + const nameToId: Record = {}; + 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() { + Stack() { + // WebView 显示 3D 星图 + Web({ src: $rawfile('graph.html'), controller: this.controller }) + .javaScriptAccess(true) + .width('100%') + .height('100%') + .zoomAccess(true) // 启用缩放(自带双指捏合) + .onPageEnd(() => { + this.pushGraphDataToWebView(); + }) + // 注册原生桥接对象,供 WebView JavaScript 调用 + .javaScriptProxy({ + object: this.bridge, + name: 'nativeBridge', + methodList: ['onNodeClick', 'onSearch'], + asyncMethodList: ['requestGraphData'], + controller: this.controller + }) + + // 搜索框组件 - 在 WebView 上方 + Column() { + TextInput({ placeholder: '搜索节点...', text: this.searchText }) + .width('80%') + .height(40) + .backgroundColor('rgba(10, 10, 26, 0.8)') + .fontColor('#ffffff') + .placeholderColor('#666688') + .borderRadius(8) + .border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' }) + .margin({ top: 20 }) + .onChange((value: string) => { + this.onSearchInput(value); + }) + } + .width('100%') + .position({ x: 0, y: 0 }) + .zIndex(10) + + // 节点详情浮层 + if (this.showNodeDetail && this.selectedNodeDetail !== null) { + Column() { + Column() { + Text(this.selectedNodeDetail.name) + .fontSize(18) + .fontColor('#44ff88') + .fontWeight(FontWeight.Bold) + .margin({ bottom: 10 }) + + Text('类型: ' + this.selectedNodeDetail.type) + .fontSize(14) + .fontColor('#aaaacc') + + Text('提及次数: ' + this.selectedNodeDetail.mention_count) + .fontSize(14) + .fontColor('#aaaacc') + + Text('连接数: ' + this.selectedNodeDetail.connection_count) + .fontSize(14) + .fontColor('#aaaacc') + + if (this.selectedNodeDetail.connections && this.selectedNodeDetail.connections.length > 0) { + Text('连接关系:') + .fontSize(14) + .fontColor('#8888aa') + .margin({ top: 10, bottom: 5 }) + List() { + ForEach(this.selectedNodeDetail.connections, (conn: ConnectionItem) => { + ListItem() { + Text(conn.type + ': ' + conn.target_name) + .fontSize(12) + .fontColor('#aaaacc') + } + }) + } + .height(100) + } + + Button('关闭') + .width(80) + .height(30) + .margin({ top: 15 }) + .backgroundColor('rgba(100, 100, 255, 0.3)') + .fontColor('#ffffff') + .onClick(() => { + this.closeNodeDetail(); + }) + } + .padding(20) + .backgroundColor('rgba(10, 10, 26, 0.95)') + .borderRadius(12) + .border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' }) + .width(300) + } + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .zIndex(20) + } + } + .width('100%') + .height('100%') + } +} diff --git a/features/settings/Index.ets b/features/settings/Index.ets new file mode 100644 index 0000000..869f773 --- /dev/null +++ b/features/settings/Index.ets @@ -0,0 +1 @@ +export { SettingsPage } from './src/main/ets/pages/SettingsPage'; diff --git a/features/settings/build-profile.json5 b/features/settings/build-profile.json5 new file mode 100644 index 0000000..b823c6e --- /dev/null +++ b/features/settings/build-profile.json5 @@ -0,0 +1,8 @@ +{ + "apiType": "stageMode", + "targets": [ + { + "name": "default" + } + ] +} diff --git a/features/settings/oh-package.json5 b/features/settings/oh-package.json5 new file mode 100644 index 0000000..f5cbfc6 --- /dev/null +++ b/features/settings/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "@ohos/settings", + "version": "1.0.0", + "description": "TrulyMEM settings feature module", + "main": "Index.ets", + "author": "", + "license": "", + "dependencies": { + "@ohos/common": "file:../../common" + } +} diff --git a/features/settings/src/main/ets/pages/SettingsPage.ets b/features/settings/src/main/ets/pages/SettingsPage.ets new file mode 100644 index 0000000..c351bb4 --- /dev/null +++ b/features/settings/src/main/ets/pages/SettingsPage.ets @@ -0,0 +1,126 @@ +import dataPreferences from '@ohos.data.preferences'; + +@Component +export struct SettingsPage { + @State baseUrl: string = ''; + @State model: string = ''; + @State apiKey: string = ''; + private pref?: dataPreferences.Preferences; + + async aboutToAppear() { + const ctx = getContext(this); + this.pref = await dataPreferences.getPreferences(ctx, 'trulymem_config'); + this.baseUrl = String(await this.pref.get('base_url', 'https://api.deepseek.com')); + this.model = String(await this.pref.get('model', 'deepseek-chat')); + this.apiKey = String(await this.pref.get('api_key', '')); + } + + async onBaseUrlChange(value: string) { + this.baseUrl = value; + await this.pref?.put('base_url', value); + await this.pref?.flush(); + } + + async onModelChange(value: string) { + this.model = value; + await this.pref?.put('model', value); + await this.pref?.flush(); + } + + async onApiKeyChange(value: string) { + this.apiKey = value; + await this.pref?.put('api_key', value); + await this.pref?.flush(); + } + + build() { + Stack() { + // 主题色光晕背景 + Column() + .width(200) + .height(200) + .backgroundColor('rgba(124,77,255,0.15)') + .blur(40) + .borderRadius(100) + .position({ x: '10%', y: '20%' }) + + Column() { + Text('API 配置') + .fontSize(24) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .margin({ top: 20, bottom: 16 }) + + // Base URL 设置项 + Column() { + Text('Base URL') + .fontSize(14) + .fontColor('#FFFFFF') + .width('100%') + .margin({ bottom: 8 }) + + 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%') + .backgroundColor('rgba(26,27,46,0.95)') + .backgroundBlurStyle(BlurStyle.Regular) + } +} diff --git a/products/phone/build-profile.json5 b/products/phone/build-profile.json5 new file mode 100644 index 0000000..b823c6e --- /dev/null +++ b/products/phone/build-profile.json5 @@ -0,0 +1,8 @@ +{ + "apiType": "stageMode", + "targets": [ + { + "name": "default" + } + ] +} diff --git a/products/phone/oh-package.json5 b/products/phone/oh-package.json5 new file mode 100644 index 0000000..b192f58 --- /dev/null +++ b/products/phone/oh-package.json5 @@ -0,0 +1,14 @@ +{ + "name": "@ohos/phone", + "version": "1.0.0", + "description": "TrulyMEM phone product entry", + "main": "Index.ets", + "author": "", + "license": "", + "dependencies": { + "@ohos/common": "file:../../common", + "@ohos/graph": "file:../../features/graph", + "@ohos/chat": "file:../../features/chat", + "@ohos/settings": "file:../../features/settings" + } +} diff --git a/products/phone/src/main/ets/entryability/EntryAbility.ets b/products/phone/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..6e60172 --- /dev/null +++ b/products/phone/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,35 @@ +import UIAbility from '@ohos.app.ability.UIAbility'; +import window from '@ohos.window'; + +export default class EntryAbility extends UIAbility { + onCreate(want, launchParam) { + console.info('EntryAbility onCreate'); + } + + onDestroy() { + console.info('EntryAbility onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + console.info('EntryAbility onWindowStageCreate'); + windowStage.loadContent('pages/Index', (err, data) => { + if (err.code) { + console.error('Failed to load the content. Cause: ' + JSON.stringify(err)); + return; + } + console.info('Succeeded in loading the content. Data: ' + JSON.stringify(data)); + }); + } + + onWindowStageDestroy() { + console.info('EntryAbility onWindowStageDestroy'); + } + + onForeground() { + console.info('EntryAbility onForeground'); + } + + onBackground() { + console.info('EntryAbility onBackground'); + } +} diff --git a/products/phone/src/main/ets/pages/Index.ets b/products/phone/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..423f410 --- /dev/null +++ b/products/phone/src/main/ets/pages/Index.ets @@ -0,0 +1,125 @@ +import { GraphDatabase } from '@ohos/common'; +import { GraphPage } from '@ohos/graph'; +import { ChatPage } from '@ohos/chat'; +import { SettingsPage } from '@ohos/settings'; +import { ImmersiveTabNavigation } from '@ohos/common'; +import display from '@ohos.display'; + +@Entry +@Component +struct Index { + @State currentIndex: number = 0; + private db: GraphDatabase = new GraphDatabase(); + private displayCallback?: Callback; + + aboutToAppear() { + this.db.init(getContext(this)); + } + + aboutToDisappear() { + if (this.displayCallback) { + display.off('change', this.displayCallback); + } + } + + @Builder + tabContentBuilder() { + Column() { + if (this.currentIndex === 0) { + MainPage({ db: this.db }) + } else { + SettingsPage() + } + } + .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%') + } +} + +@Component +struct MainPage { + @Prop db: GraphDatabase; + @State isWide: boolean = false; + + aboutToAppear() { + this.updateBreakpoint(); + try { + display.on('change', () => { + this.updateBreakpoint(); + }); + } catch (e) { + console.error('display.on error: ' + JSON.stringify(e)); + } + } + + private updateBreakpoint(): void { + try { + const defaultWindow = display.getDefaultDisplaySync(); + this.isWide = defaultWindow.width > 520; + } catch (e) { + console.error('updateBreakpoint error: ' + JSON.stringify(e)); + } + } + + build() { + if (this.isWide) { + Row() { + GraphPage({ db: this.db }) + .layoutWeight(1) + .height('100%') + .clip(true) + .borderRadius(12) + .margin({ left: 4, right: 2 }) + + ChatPage({ db: this.db }) + .width(380) + .height('100%') + .clip(true) + .borderRadius(12) + .margin({ left: 2, right: 4 }) + } + .width('100%') + .height('100%') + .padding(4) + .backgroundColor('#1A1B2E') + .backgroundBlurStyle(BlurStyle.Regular) + } else { + Column() { + Stack() { + GraphPage({ db: this.db }) + } + .height('55%') + .width('100%') + .clip(true) + .borderRadius(12) + .margin({ top: 2, left: 4, right: 4, bottom: 2 }) + + Stack() { + ChatPage({ db: this.db }) + } + .height('45%') + .width('100%') + .clip(true) + .borderRadius(12) + .margin({ top: 2, left: 4, right: 4, bottom: 2 }) + } + .width('100%') + .height('100%') + .padding(2) + .backgroundColor('#1A1B2E') + .backgroundBlurStyle(BlurStyle.Regular) + } + } +} diff --git a/products/phone/src/main/module.json5 b/products/phone/src/main/module.json5 new file mode 100644 index 0000000..9c4fcde --- /dev/null +++ b/products/phone/src/main/module.json5 @@ -0,0 +1,46 @@ +{ + "module": { + "name": "phone", + "type": "entry", + "description": "TrulyMEM phone entry module", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "ohos.want.action.home" + ] + } + ] + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + }, + { + "name": "ohos.permission.GET_NETWORK_INFO" + } + ] + } +} diff --git a/products/phone/src/main/resources/base/profile/main_pages.json b/products/phone/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/products/phone/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +}