From 5ae2143822ea1ca985c5e864fac23a9b3142a9b6 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 09:21:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B9=E7=94=A8=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=20Tabs=20=E5=AF=BC=E8=88=AA=EF=BC=8C=E6=B8=85?= =?UTF-8?q?=E7=90=86=E6=97=A7=20entry=20=E7=9B=AE=E5=BD=95=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MainPage 改用 ArkUI Tabs + BottomTabBarStyle 原生导航 - 移除 ImmersiveTabNavigation 自定义组件依赖 - 清理旧 entry/src/main/ets/ 下的已迁移文件 - 更新 common/Index.ets 移除 ImmersiveTabNavigation 导出 --- common/Index.ets | 1 - entry/src/main/ets/common/Constants.ets | 44 - .../ets/components/ImmersiveTabNavigation.ets | 132 -- entry/src/main/ets/model/GraphDatabase.ets | 936 --------------- entry/src/main/ets/pages/ChatPage.ets | 171 --- entry/src/main/ets/pages/GraphPage.ets | 313 ----- entry/src/main/ets/pages/Index.ets | 48 - entry/src/main/ets/pages/MainPage.ets | 82 -- entry/src/main/ets/pages/SettingsPage.ets | 126 -- .../src/main/ets/services/AIAgentService.ets | 880 -------------- .../main/ets/services/GraphMemoryService.ets | 1067 ----------------- .../phone/src/main/ets/pages/MainPage.ets | 180 +-- 12 files changed, 91 insertions(+), 3889 deletions(-) delete mode 100644 entry/src/main/ets/common/Constants.ets delete mode 100644 entry/src/main/ets/components/ImmersiveTabNavigation.ets delete mode 100644 entry/src/main/ets/model/GraphDatabase.ets delete mode 100644 entry/src/main/ets/pages/ChatPage.ets delete mode 100644 entry/src/main/ets/pages/GraphPage.ets delete mode 100644 entry/src/main/ets/pages/Index.ets delete mode 100644 entry/src/main/ets/pages/MainPage.ets delete mode 100644 entry/src/main/ets/pages/SettingsPage.ets delete mode 100644 entry/src/main/ets/services/AIAgentService.ets delete mode 100644 entry/src/main/ets/services/GraphMemoryService.ets diff --git a/common/Index.ets b/common/Index.ets index 2e11e46..fa3c683 100644 --- a/common/Index.ets +++ b/common/Index.ets @@ -2,7 +2,6 @@ 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'; export { PageContext, RouterParam, IPageContext } from './src/main/ets/routermanager/PageContext'; export { BreakpointType, BreakpointTypes, WidthBreakpoint } from './src/main/ets/util/BreakpointSystem'; export { BaseViewModel, VMEvent } from './src/main/ets/viewmodel/BaseViewModel'; diff --git a/entry/src/main/ets/common/Constants.ets b/entry/src/main/ets/common/Constants.ets deleted file mode 100644 index 374db53..0000000 --- a/entry/src/main/ets/common/Constants.ets +++ /dev/null @@ -1,44 +0,0 @@ -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/entry/src/main/ets/components/ImmersiveTabNavigation.ets b/entry/src/main/ets/components/ImmersiveTabNavigation.ets deleted file mode 100644 index 739673e..0000000 --- a/entry/src/main/ets/components/ImmersiveTabNavigation.ets +++ /dev/null @@ -1,132 +0,0 @@ -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/entry/src/main/ets/model/GraphDatabase.ets b/entry/src/main/ets/model/GraphDatabase.ets deleted file mode 100644 index 58e96e3..0000000 --- a/entry/src/main/ets/model/GraphDatabase.ets +++ /dev/null @@ -1,936 +0,0 @@ -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/entry/src/main/ets/pages/ChatPage.ets b/entry/src/main/ets/pages/ChatPage.ets deleted file mode 100644 index 55e6c79..0000000 --- a/entry/src/main/ets/pages/ChatPage.ets +++ /dev/null @@ -1,171 +0,0 @@ -import { GraphDatabase } from '../model/GraphDatabase'; -import { GraphMemoryService } from '../services/GraphMemoryService'; -import { AIAgentService, ChatMessage, AgentResponse } from '../services/AIAgentService'; - -@Component -export struct ChatPage { - @State messages: ChatMessage[] = []; - @State inputText: string = ''; - @Prop db: GraphDatabase; - @State toolCallLog: string = ''; - @State isThinking: boolean = false; - private scrollController: Scroller = new Scroller(); - private agentService?: AIAgentService; - - async aboutToAppear() { - // 初始化图记忆服务和 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/entry/src/main/ets/pages/GraphPage.ets b/entry/src/main/ets/pages/GraphPage.ets deleted file mode 100644 index 9dc5b7e..0000000 --- a/entry/src/main/ets/pages/GraphPage.ets +++ /dev/null @@ -1,313 +0,0 @@ -/** - * GraphPage — 记忆星图页面 - * 使用 WebView 显示 Three.js 3D 图可视化 - * 通过 javaScriptProxy 与 WebView 双向通信 - */ -import web_webview from '@ohos.web.webview'; -import { GraphDatabase, RecallEntity } from '../model/GraphDatabase'; -import { GraphMemoryService, ConnectionItem, NodeDetailInfo } from '../services/GraphMemoryService'; - -// ========= 图数据结构定义 ========= - -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/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets deleted file mode 100644 index 28d5381..0000000 --- a/entry/src/main/ets/pages/Index.ets +++ /dev/null @@ -1,48 +0,0 @@ -import { MainPage } from './MainPage'; -import { SettingsPage } from './SettingsPage'; -import { GraphDatabase } from '../model/GraphDatabase'; -import { ImmersiveTabNavigation } from '../components/ImmersiveTabNavigation'; -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%') - } -} diff --git a/entry/src/main/ets/pages/MainPage.ets b/entry/src/main/ets/pages/MainPage.ets deleted file mode 100644 index 741e878..0000000 --- a/entry/src/main/ets/pages/MainPage.ets +++ /dev/null @@ -1,82 +0,0 @@ -import { GraphDatabase } from '../model/GraphDatabase'; -import { GraphPage } from './GraphPage'; -import { ChatPage } from './ChatPage'; -import display from '@ohos.display'; - -@Component -export 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) { - // ======== 宽屏模式(>520px):左侧星图 + 右侧聊天 ======== - 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 { - // ======== 窄屏模式(≤520px):上方星图(55%) + 下方聊天(45%) ======== - 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/entry/src/main/ets/pages/SettingsPage.ets b/entry/src/main/ets/pages/SettingsPage.ets deleted file mode 100644 index c351bb4..0000000 --- a/entry/src/main/ets/pages/SettingsPage.ets +++ /dev/null @@ -1,126 +0,0 @@ -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/entry/src/main/ets/services/AIAgentService.ets b/entry/src/main/ets/services/AIAgentService.ets deleted file mode 100644 index 030a9a5..0000000 --- a/entry/src/main/ets/services/AIAgentService.ets +++ /dev/null @@ -1,880 +0,0 @@ -/** - * 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/entry/src/main/ets/services/GraphMemoryService.ets b/entry/src/main/ets/services/GraphMemoryService.ets deleted file mode 100644 index a74323d..0000000 --- a/entry/src/main/ets/services/GraphMemoryService.ets +++ /dev/null @@ -1,1067 +0,0 @@ -/** - * 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/products/phone/src/main/ets/pages/MainPage.ets b/products/phone/src/main/ets/pages/MainPage.ets index ea8b949..2a3a5b0 100644 --- a/products/phone/src/main/ets/pages/MainPage.ets +++ b/products/phone/src/main/ets/pages/MainPage.ets @@ -2,103 +2,105 @@ 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'; @Component export struct MainPage { - @Prop db: GraphDatabase; - @State isWide: boolean = false; - @State currentIndex: number = 0; + @Prop db: GraphDatabase; + @State isWide: boolean = false; + @State currentIndex: number = 0; + private tabController: TabsController = new TabsController(); - aboutToAppear() { - this.updateBreakpoint(); - try { - display.on('change', () => { + aboutToAppear() { 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)); - } - } - - @Builder - tabContentBuilder() { - Column() { - if (this.currentIndex === 0) { - 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'); - } 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'); + try { + display.on('change', () => { this.updateBreakpoint(); }); + } catch (e) { + console.error('display.on error: ' + JSON.stringify(e)); } - } else { - SettingsPage() - } } - .width('100%') - .height('100%'); - } - build() { - Stack() { - ImmersiveTabNavigation({ - currentIndex: this.currentIndex, - onTabChange: (index: number): void => { - this.currentIndex = index; - }, - contentBuilder: (): void => { - this.tabContentBuilder(); + private updateBreakpoint(): void { + try { + const defaultWindow = display.getDefaultDisplaySync(); + this.isWide = defaultWindow.width > 520; + } catch (e) { + console.error('updateBreakpoint error: ' + JSON.stringify(e)); } - }) } - .width('100%') - .height('100%'); - } -} + + @Builder + TabContentsBuilder() { + TabContent() { + 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') + } 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') + } + } + .tabBar(this.bottomTabBarStyle('TrulyMEM', '\ue8b6')) + + TabContent() { + SettingsPage() + } + .tabBar(this.bottomTabBarStyle('设置', '\ue8b8')) + } + + private bottomTabBarStyle(title: string, icon: string): BottomTabBarStyle { + return new BottomTabBarStyle({ + normal: new SymbolGlyphModifier(icon) + }, title) + .padding({ bottom: 4 }); + } + + build() { + Tabs({ controller: this.tabController, index: this.currentIndex }) { + this.TabContentsBuilder() + } + .width('100%') + .height('100%') + .backgroundColor('#0A0A1A') + .barPosition(BarPosition.End) + .scrollable(false) + .animationMode(AnimationMode.NO_ANIMATION) + .onChange((index: number) => { + this.currentIndex = index; + }) + } +} \ No newline at end of file