mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-09-21 17:38:18 +00:00
refactor: 按 sample_in_harmonyos 多层模块化架构重构工程结构
- 新增 common/ 公共模块:模型、服务、组件、常量 - 新增 features/ 功能模块:graph(星图)、chat(聊天)、settings(设置)、commonbusiness - 新增 products/phone 产品入口层:EntryAbility + Index + MainPage - Index.ets 简化为纯路由入口,TabNavigation 移至 MainPage 统一管理 - 更新 build-profile.json5 注册所有新模块 - 迁移原始 entry/ 代码到分层架构,保持功能完整
This commit is contained in:
@ -24,13 +24,87 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"modules": [
|
"modules": [
|
||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"srcPath": "./common",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "commonbusiness",
|
||||||
|
"srcPath": "./features/commonbusiness",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "graph",
|
||||||
|
"srcPath": "./features/graph",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chat",
|
||||||
|
"srcPath": "./features/chat",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "settings",
|
||||||
|
"srcPath": "./features/settings",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phone",
|
||||||
|
"srcPath": "./products/phone",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "entry",
|
"name": "entry",
|
||||||
"srcPath": "./entry",
|
"srcPath": "./entry",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"name": "default",
|
||||||
"applyToProducts": ["default"]
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -39,7 +113,10 @@
|
|||||||
"srcPath": "./trulymem-core",
|
"srcPath": "./trulymem-core",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"name": "default"
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
5
common/Index.ets
Normal file
5
common/Index.ets
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { TrulyMEMConstants } from './src/main/ets/constant/TrulyMEMConstants';
|
||||||
|
export { GraphDatabase, RecallEntity, TimeRangeParams } from './src/main/ets/model/GraphDatabase';
|
||||||
|
export { GraphMemoryService } from './src/main/ets/service/GraphMemoryService';
|
||||||
|
export { AIAgentService, ChatMessage, AgentResponse } from './src/main/ets/service/AIAgentService';
|
||||||
|
export { ImmersiveTabNavigation } from './src/main/ets/component/ImmersiveTabNavigation';
|
||||||
8
common/build-profile.json5
Normal file
8
common/build-profile.json5
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
common/oh-package.json5
Normal file
9
common/oh-package.json5
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM common module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
132
common/src/main/ets/component/ImmersiveTabNavigation.ets
Normal file
132
common/src/main/ets/component/ImmersiveTabNavigation.ets
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { window } from '@kit.ArkUI';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
|
||||||
|
const THEME_COLOR = '#7C4DFF';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct ImmersiveTabNavigation {
|
||||||
|
@State currentIndex: number = 0;
|
||||||
|
@BuilderParam contentBuilder: () => void;
|
||||||
|
onTabChange?: (index: number) => void;
|
||||||
|
|
||||||
|
private windowFocused: boolean = true;
|
||||||
|
private bottomAvoidHeight: number = 0;
|
||||||
|
|
||||||
|
aboutToAppear() {
|
||||||
|
const mainWindow = AppStorage.get<window.Window>('main_window');
|
||||||
|
if (mainWindow) {
|
||||||
|
try {
|
||||||
|
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
|
||||||
|
this.bottomAvoidHeight = avoidArea.bottomRect.height || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get avoid area: ' + (e as BusinessError).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerTabSwitchFeedback(index: number) {
|
||||||
|
this.currentIndex = index;
|
||||||
|
AppStorage.setOrCreate('global_theme_color', THEME_COLOR);
|
||||||
|
this.onTabChange?.(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
tabBarBuilder(index: number, icon: string, label: string) {
|
||||||
|
Column() {
|
||||||
|
if (this.currentIndex === index && this.windowFocused) {
|
||||||
|
Circle()
|
||||||
|
.width(32)
|
||||||
|
.height(32)
|
||||||
|
.backgroundColor(`${THEME_COLOR}33`)
|
||||||
|
.blur(8)
|
||||||
|
.position({ x: '50%', y: '50%' })
|
||||||
|
.translate({ x: '-50%', y: '-50%' })
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(icon)
|
||||||
|
.fontSize(20)
|
||||||
|
.opacity(this.currentIndex === index ? 1 : 0.5)
|
||||||
|
|
||||||
|
Text(label)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor(this.currentIndex === index ? THEME_COLOR : '#999')
|
||||||
|
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(56)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
.alignItems(HorizontalAlign.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Column() {
|
||||||
|
this.contentBuilder()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
Stack() {
|
||||||
|
Column()
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
.borderRadius(24)
|
||||||
|
|
||||||
|
Column()
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor(`${THEME_COLOR}0D`)
|
||||||
|
.borderRadius(24)
|
||||||
|
|
||||||
|
Column()
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.linearGradient({
|
||||||
|
angle: 180,
|
||||||
|
colors: [['rgba(255,255,255,0.15)', 0.0], ['rgba(255,255,255,0.05)', 1.0]]
|
||||||
|
})
|
||||||
|
.borderRadius(24)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
|
||||||
|
Tabs({ index: this.currentIndex }) {
|
||||||
|
TabContent() {
|
||||||
|
Column() {
|
||||||
|
Blank()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabBar(this.tabBarBuilder(0, '🌌', 'TrulyMEM'))
|
||||||
|
|
||||||
|
TabContent() {
|
||||||
|
Column() {
|
||||||
|
Blank()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabBar(this.tabBarBuilder(1, '⚙', '设置'))
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(64)
|
||||||
|
.barPosition(BarPosition.End)
|
||||||
|
.onChange((index: number) => {
|
||||||
|
this.triggerTabSwitchFeedback(index);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('92%')
|
||||||
|
.height(72)
|
||||||
|
.alignSelf(ItemAlign.Center)
|
||||||
|
.position({ y: `calc(100% - ${this.bottomAvoidHeight > 0 ? this.bottomAvoidHeight : 16}px - 72px)` })
|
||||||
|
.borderRadius(24)
|
||||||
|
.shadow({
|
||||||
|
radius: 20,
|
||||||
|
offsetY: -4,
|
||||||
|
color: 'rgba(0,0,0,0.15)'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('#00000000')
|
||||||
|
}
|
||||||
|
}
|
||||||
44
common/src/main/ets/constant/TrulyMEMConstants.ets
Normal file
44
common/src/main/ets/constant/TrulyMEMConstants.ets
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export class Constants {
|
||||||
|
static readonly DB_NAME: string = 'trulymem.db';
|
||||||
|
static readonly CONFIG_PREF_NAME: string = 'trulymem_config';
|
||||||
|
static readonly DEFAULT_BASE_URL: string = 'https://api.deepseek.com';
|
||||||
|
static readonly DEFAULT_MODEL: string = 'deepseek-chat';
|
||||||
|
static readonly SECURITY_LEVEL: number = 1; // S1
|
||||||
|
|
||||||
|
// Table names
|
||||||
|
static readonly TABLE_NODES: string = 'nodes';
|
||||||
|
static readonly TABLE_RELATIONS: string = 'relations';
|
||||||
|
static readonly TABLE_CHAT: string = 'chat_records';
|
||||||
|
|
||||||
|
// SQL definitions
|
||||||
|
static readonly SQL_CREATE_NODES: string = `
|
||||||
|
CREATE TABLE IF NOT EXISTS nodes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
type TEXT DEFAULT 'concept',
|
||||||
|
mentions INTEGER DEFAULT 1,
|
||||||
|
created_at TEXT DEFAULT (datetime('now','localtime')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now','localtime'))
|
||||||
|
)`;
|
||||||
|
|
||||||
|
static readonly SQL_CREATE_RELATIONS: string = `
|
||||||
|
CREATE TABLE IF NOT EXISTS relations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
subject_id INTEGER NOT NULL,
|
||||||
|
relation TEXT NOT NULL,
|
||||||
|
object_id INTEGER NOT NULL,
|
||||||
|
weight REAL DEFAULT 1.0,
|
||||||
|
created_at TEXT DEFAULT (datetime('now','localtime')),
|
||||||
|
FOREIGN KEY (subject_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (object_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||||
|
)`;
|
||||||
|
|
||||||
|
static readonly SQL_CREATE_CHAT: string = `
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
tools TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||||
|
)`;
|
||||||
|
}
|
||||||
936
common/src/main/ets/model/GraphDatabase.ets
Normal file
936
common/src/main/ets/model/GraphDatabase.ets
Normal file
@ -0,0 +1,936 @@
|
|||||||
|
import relationalStore from '@ohos.data.relationalStore';
|
||||||
|
import { Context } from '@ohos.abilityAccessCtrl';
|
||||||
|
|
||||||
|
interface NodeNameCacheItem { name: string; type: string; mentions: number; }
|
||||||
|
export interface TimeRangeParams { days: number; }
|
||||||
|
|
||||||
|
interface TripletData {
|
||||||
|
subject: string;
|
||||||
|
relation: string;
|
||||||
|
object: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CriteriaData {
|
||||||
|
subject?: string;
|
||||||
|
target?: string;
|
||||||
|
relation?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeData {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EdgeData {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
label: string;
|
||||||
|
weight: number;
|
||||||
|
depth?: number;
|
||||||
|
sessionId?: string;
|
||||||
|
turnId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphData {
|
||||||
|
nodes: NodeData[];
|
||||||
|
edges: EdgeData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecallEntity {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mention_count: number;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecallRelation {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
type: string;
|
||||||
|
confidence: number;
|
||||||
|
session_id?: string;
|
||||||
|
turn_id?: number;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecallResult {
|
||||||
|
entities: RecallEntity[];
|
||||||
|
relations: RecallRelation[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BfsEntity {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelationQueryResult {
|
||||||
|
sourceId: number;
|
||||||
|
targetId: number;
|
||||||
|
sourceName: string;
|
||||||
|
targetName: string;
|
||||||
|
type: string;
|
||||||
|
confidence: number;
|
||||||
|
sessionId?: string;
|
||||||
|
turnId?: number;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeQueryResult {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CleanupResult {
|
||||||
|
cleaned: number;
|
||||||
|
deleted_relations?: number;
|
||||||
|
deleted_orphans?: number;
|
||||||
|
dry_run?: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntrospectResult {
|
||||||
|
entity_count: number;
|
||||||
|
relation_count: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArchiveResult {
|
||||||
|
archived: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResultItem {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
session_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SnapshotData {
|
||||||
|
entities: RecallEntity[];
|
||||||
|
relations: RecallRelation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORE_CONFIG: relationalStore.StoreConfig = {
|
||||||
|
name: 'trulymem.db',
|
||||||
|
securityLevel: relationalStore.SecurityLevel.S1
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GraphDatabase {
|
||||||
|
private store?: relationalStore.RdbStore;
|
||||||
|
private context?: Context;
|
||||||
|
|
||||||
|
async init(context: Context): Promise<void> {
|
||||||
|
this.context = context;
|
||||||
|
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
|
||||||
|
await this.createTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createTables(): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
await this.store.executeSql(`
|
||||||
|
CREATE TABLE IF NOT EXISTS nodes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
type TEXT DEFAULT 'concept',
|
||||||
|
mentions INTEGER DEFAULT 1,
|
||||||
|
created_at TEXT,
|
||||||
|
updated_at TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await this.store.executeSql(`
|
||||||
|
CREATE TABLE IF NOT EXISTS relations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
subject_id INTEGER NOT NULL,
|
||||||
|
relation TEXT NOT NULL,
|
||||||
|
object_id INTEGER NOT NULL,
|
||||||
|
weight REAL DEFAULT 1.0,
|
||||||
|
session_id TEXT,
|
||||||
|
turn_id INTEGER,
|
||||||
|
created_at TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
date_bucket TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await this.store.executeSql(`
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_id TEXT,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
tools TEXT,
|
||||||
|
created_at TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_name ON nodes(name)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_type ON nodes(type)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_source ON relations(subject_id)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_target ON relations(object_id)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_type ON relations(relation)`);
|
||||||
|
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_status ON relations(status)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit(triplets: TripletData[], entityTypes?: Record<string, string>, sessionId?: string, turnId?: number): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
for (const triplet of triplets) {
|
||||||
|
const subjectId: number = await this.upsertNode(triplet.subject, entityTypes?.[triplet.subject]);
|
||||||
|
const objectId: number = await this.upsertNode(triplet.object, entityTypes?.[triplet.object]);
|
||||||
|
const existingId: number = await this.checkDuplicateRelation(subjectId, triplet.relation, objectId);
|
||||||
|
if (existingId > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const dateBucket = now.split('T')[0];
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'subject_id': subjectId,
|
||||||
|
'relation': triplet.relation,
|
||||||
|
'object_id': objectId,
|
||||||
|
'session_id': sessionId || null,
|
||||||
|
'turn_id': turnId || null,
|
||||||
|
'created_at': now,
|
||||||
|
'updated_at': now,
|
||||||
|
'status': 'active',
|
||||||
|
'date_bucket': dateBucket
|
||||||
|
};
|
||||||
|
await this.store.insert('relations', bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkDuplicateRelation(subjectId: number, relation: string, objectId: number): Promise<number> {
|
||||||
|
if (!this.store) return -1;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('subject_id', subjectId).and().equalTo('relation', relation).and().equalTo('object_id', objectId).and().equalTo('status', 'active');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
|
||||||
|
if (resultSet.goToFirstRow()) {
|
||||||
|
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
resultSet.close();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertNode(name: string, entityType?: string): Promise<number> {
|
||||||
|
if (!this.store) return -1;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('name', name);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'mentions']);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
if (resultSet.goToNextRow()) {
|
||||||
|
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
const mentions: number = resultSet.getLong(resultSet.getColumnIndex('mentions'));
|
||||||
|
resultSet.close();
|
||||||
|
const updatePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
updatePredicates.equalTo('id', id);
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'mentions': mentions + 1,
|
||||||
|
'updated_at': now
|
||||||
|
};
|
||||||
|
await this.store.update(bucket, updatePredicates);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'name': name,
|
||||||
|
'type': entityType || 'concept',
|
||||||
|
'mentions': 1,
|
||||||
|
'created_at': now,
|
||||||
|
'updated_at': now
|
||||||
|
};
|
||||||
|
return await this.store.insert('nodes', bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async recall(queryIntent: string, seedEntities?: string[], depth: number = 2, timeRange?: TimeRangeParams, sessionFilter?: string): Promise<RecallResult> {
|
||||||
|
if (!this.store) {
|
||||||
|
return { entities: [], relations: [], message: 'Database not initialized' };
|
||||||
|
}
|
||||||
|
// 计算时间范围过滤
|
||||||
|
let minDateBucket: string | undefined;
|
||||||
|
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||||
|
minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
}
|
||||||
|
const keywords = queryIntent.toLowerCase().replace(/,/g, ' ').split(/\s+/).filter(w => w.trim());
|
||||||
|
const allEntities: BfsEntity[] = [];
|
||||||
|
const entityIds = new Set<number>();
|
||||||
|
let seedEntityIds = new Set<number>();
|
||||||
|
|
||||||
|
if (keywords.length === 0 && (!seedEntities || seedEntities.length === 0)) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.orderByDesc('mentions');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
while (resultSet.goToNextRow() && allEntities.length < 50) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
const name = resultSet.getString(resultSet.getColumnIndex('name'));
|
||||||
|
const type = resultSet.getString(resultSet.getColumnIndex('type'));
|
||||||
|
const mentions = resultSet.getLong(resultSet.getColumnIndex('mentions'));
|
||||||
|
entityIds.add(id);
|
||||||
|
allEntities.push({ id, name, type, mentions, depth: 0 });
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
} else {
|
||||||
|
if (seedEntities && seedEntities.length > 0) {
|
||||||
|
for (const seedName of seedEntities) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('name', seedName);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
if (!entityIds.has(id)) {
|
||||||
|
entityIds.add(id);
|
||||||
|
seedEntityIds.add(id);
|
||||||
|
allEntities.push({
|
||||||
|
id,
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||||
|
depth: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.like('name', `%${keyword}%`);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
if (!entityIds.has(id)) {
|
||||||
|
entityIds.add(id);
|
||||||
|
allEntities.push({
|
||||||
|
id,
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||||
|
depth: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRelations: RelationQueryResult[] = [];
|
||||||
|
let currentLayerIds = new Set<number>(entityIds);
|
||||||
|
const visitedEntityIds = new Set<number>(entityIds);
|
||||||
|
// 批量预加载所有相关节点名称,减少 N+1 查询
|
||||||
|
const nodeNameCache = new Map<number, NodeNameCacheItem>();
|
||||||
|
|
||||||
|
for (let layer = 0; layer < depth && currentLayerIds.size > 0; layer++) {
|
||||||
|
const currentIds = Array.from(currentLayerIds);
|
||||||
|
const relations = await this.getRelationsForNodes(currentIds, sessionFilter, minDateBucket);
|
||||||
|
const nextLayerIds = new Set<number>();
|
||||||
|
|
||||||
|
for (const rel of relations) {
|
||||||
|
allRelations.push(rel);
|
||||||
|
if (!visitedEntityIds.has(rel.targetId)) {
|
||||||
|
nextLayerIds.add(rel.targetId);
|
||||||
|
}
|
||||||
|
if (rel.targetId !== rel.sourceId && !visitedEntityIds.has(rel.sourceId)) {
|
||||||
|
nextLayerIds.add(rel.sourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const newId of nextLayerIds) {
|
||||||
|
if (!visitedEntityIds.has(newId)) {
|
||||||
|
visitedEntityIds.add(newId);
|
||||||
|
// 优先从缓存获取,避免 N+1 查询
|
||||||
|
const cached = nodeNameCache.get(newId);
|
||||||
|
if (cached) {
|
||||||
|
const addedEntity: BfsEntity = {
|
||||||
|
id: newId,
|
||||||
|
name: cached.name,
|
||||||
|
type: cached.type,
|
||||||
|
mentions: cached.mentions,
|
||||||
|
depth: layer + 1
|
||||||
|
};
|
||||||
|
allEntities.push(addedEntity);
|
||||||
|
} else {
|
||||||
|
const nodeData = await this.getNodeById(newId);
|
||||||
|
if (nodeData) {
|
||||||
|
const cacheItem: NodeNameCacheItem = { name: nodeData.name, type: nodeData.type, mentions: nodeData.mentions };
|
||||||
|
nodeNameCache.set(newId, cacheItem);
|
||||||
|
const addedEntity: BfsEntity = {
|
||||||
|
id: nodeData.id,
|
||||||
|
name: nodeData.name,
|
||||||
|
type: nodeData.type,
|
||||||
|
mentions: nodeData.mentions,
|
||||||
|
depth: layer + 1
|
||||||
|
};
|
||||||
|
allEntities.push(addedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLayerIds = nextLayerIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities: RecallEntity[] = allEntities.map(e => {
|
||||||
|
const entity: RecallEntity = {
|
||||||
|
name: e.name,
|
||||||
|
type: e.type,
|
||||||
|
mention_count: e.mentions,
|
||||||
|
depth: e.depth
|
||||||
|
};
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
const relations: RecallRelation[] = allRelations.map(r => {
|
||||||
|
const rel: RecallRelation = {
|
||||||
|
source: r.sourceName,
|
||||||
|
target: r.targetName,
|
||||||
|
type: r.type,
|
||||||
|
confidence: r.confidence,
|
||||||
|
session_id: r.sessionId,
|
||||||
|
turn_id: r.turnId,
|
||||||
|
depth: r.depth
|
||||||
|
};
|
||||||
|
return rel;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
entities,
|
||||||
|
relations,
|
||||||
|
message: `找到 ${entities.length} 个实体, ${relations.length} 条关系`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNodeById(id: number): Promise<NodeQueryResult | null> {
|
||||||
|
if (!this.store) return null;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('id', id);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
if (resultSet.goToNextRow()) {
|
||||||
|
const node: NodeQueryResult = {
|
||||||
|
id: resultSet.getLong(resultSet.getColumnIndex('id')),
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||||
|
depth: 0
|
||||||
|
};
|
||||||
|
resultSet.close();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRelationsForNodes(nodeIds: number[], sessionFilter?: string, minDateBucket?: string): Promise<RelationQueryResult[]> {
|
||||||
|
if (!this.store || nodeIds.length === 0) return [];
|
||||||
|
const relations: RelationQueryResult[] = [];
|
||||||
|
// 批量预加载所有节点名称到缓存,避免 N+1 查询
|
||||||
|
const nodeNameCache = new Map<number, NodeNameCacheItem>();
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
const node = await this.getNodeById(id);
|
||||||
|
if (node) {
|
||||||
|
const cacheItem: NodeNameCacheItem = { name: node.name, type: node.type, mentions: node.mentions };
|
||||||
|
nodeNameCache.set(id, cacheItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const nodeId of nodeIds) {
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active').and().equalTo('subject_id', nodeId);
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (minDateBucket) {
|
||||||
|
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
|
||||||
|
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
relations.push({
|
||||||
|
sourceId,
|
||||||
|
targetId,
|
||||||
|
sourceName: sourceNode.name,
|
||||||
|
targetName: targetNode.name,
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||||
|
depth: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
|
||||||
|
const predicates2: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates2.equalTo('status', 'active').and().equalTo('object_id', nodeId);
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates2.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (minDateBucket) {
|
||||||
|
predicates2.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet2: relationalStore.ResultSet = await this.store.query(predicates2, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
while (resultSet2.goToNextRow()) {
|
||||||
|
const sourceId = resultSet2.getLong(resultSet2.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet2.getLong(resultSet2.getColumnIndex('object_id'));
|
||||||
|
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
|
||||||
|
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
relations.push({
|
||||||
|
sourceId,
|
||||||
|
targetId,
|
||||||
|
sourceName: sourceNode.name,
|
||||||
|
targetName: targetNode.name,
|
||||||
|
type: resultSet2.getString(resultSet2.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet2.getDouble(resultSet2.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet2.getString(resultSet2.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet2.getLong(resultSet2.getColumnIndex('turn_id')),
|
||||||
|
depth: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet2.close();
|
||||||
|
}
|
||||||
|
return relations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(keyword: string): Promise<SearchResultItem[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.like('name', `%${keyword}%`);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['name', 'type', 'mentions']);
|
||||||
|
const results: SearchResultItem[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
results.push({
|
||||||
|
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async purge(criteria: CriteriaData, mode: string = 'soft'): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
if (!criteria.subject && !criteria.target && !criteria.relation && !criteria.sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
let hasCondition = false;
|
||||||
|
|
||||||
|
if (criteria.subject) {
|
||||||
|
const subjectId = await this.getNodeIdByName(criteria.subject);
|
||||||
|
if (subjectId > 0) {
|
||||||
|
predicates.equalTo('subject_id', subjectId);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (criteria.target) {
|
||||||
|
const targetId = await this.getNodeIdByName(criteria.target);
|
||||||
|
if (targetId > 0) {
|
||||||
|
if (hasCondition) {
|
||||||
|
predicates.and();
|
||||||
|
}
|
||||||
|
predicates.equalTo('object_id', targetId);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (criteria.relation) {
|
||||||
|
if (hasCondition) {
|
||||||
|
predicates.and();
|
||||||
|
}
|
||||||
|
predicates.equalTo('relation', criteria.relation);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
if (criteria.sessionId) {
|
||||||
|
if (hasCondition) {
|
||||||
|
predicates.and();
|
||||||
|
}
|
||||||
|
predicates.equalTo('session_id', criteria.sessionId);
|
||||||
|
hasCondition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCondition) {
|
||||||
|
if (mode === 'soft') {
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'status': 'deleted',
|
||||||
|
'updated_at': new Date().toISOString()
|
||||||
|
};
|
||||||
|
await this.store.update(bucket, predicates);
|
||||||
|
} else {
|
||||||
|
await this.store.delete(predicates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.removeOrphanNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆图谱 — 在指定时间范围内查询关系和节点
|
||||||
|
* 对应 tools.memory_graph
|
||||||
|
*/
|
||||||
|
async graph(timeRange: TimeRangeParams, sessionFilter?: string): Promise<GraphData> {
|
||||||
|
if (!this.store) return { nodes: [], edges: [] };
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active');
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||||
|
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
const nodeIds = new Set<number>();
|
||||||
|
const edges: EdgeData[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
nodeIds.add(sourceId);
|
||||||
|
nodeIds.add(targetId);
|
||||||
|
const edge: EdgeData = {
|
||||||
|
from: sourceId,
|
||||||
|
to: targetId,
|
||||||
|
label: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
weight: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
|
||||||
|
};
|
||||||
|
edges.push(edge);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
const nodes: NodeData[] = [];
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
const node = await this.getNodeById(id);
|
||||||
|
if (node) {
|
||||||
|
const nodeData: NodeData = { id: node.id, label: node.name, type: node.type, mentions: node.mentions };
|
||||||
|
nodes.push(nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result: GraphData = { nodes, edges };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆快照 — 在指定时间范围内查询实体和关系
|
||||||
|
* 对应 tools.memory_snapshot
|
||||||
|
*/
|
||||||
|
async snapshot(timeRange: TimeRangeParams, sessionFilter?: string): Promise<SnapshotData> {
|
||||||
|
if (!this.store) return { entities: [], relations: [] };
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active');
|
||||||
|
if (sessionFilter) {
|
||||||
|
predicates.and().equalTo('session_id', sessionFilter);
|
||||||
|
}
|
||||||
|
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||||
|
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
const nodeIds = new Set<number>();
|
||||||
|
const relations: RecallRelation[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
nodeIds.add(sourceId);
|
||||||
|
nodeIds.add(targetId);
|
||||||
|
const sourceNode = await this.getNodeById(sourceId);
|
||||||
|
const targetNode = await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
const rel: RecallRelation = {
|
||||||
|
source: sourceNode.name,
|
||||||
|
target: targetNode.name,
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
session_id: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turn_id: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
|
||||||
|
};
|
||||||
|
relations.push(rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
const entities: RecallEntity[] = [];
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
const node = await this.getNodeById(id);
|
||||||
|
if (node) {
|
||||||
|
const recallEntity: RecallEntity = { name: node.name, type: node.type, mention_count: node.mentions };
|
||||||
|
entities.push(recallEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const snapResult: SnapshotData = { entities, relations };
|
||||||
|
return snapResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询已归档的记忆
|
||||||
|
* 对应 tools.memory_query_archived
|
||||||
|
*/
|
||||||
|
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'archived');
|
||||||
|
if (keyword) {
|
||||||
|
predicates.and().like('relation', '%' + keyword + '%');
|
||||||
|
}
|
||||||
|
if (days && days > 0) {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setDate(cutoff.getDate() - days);
|
||||||
|
const maxDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||||
|
predicates.and().lessThanOrEqualTo('date_bucket', maxDateBucket);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||||
|
const results: RelationQueryResult[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||||
|
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||||
|
const sourceNode = await this.getNodeById(sourceId);
|
||||||
|
const targetNode = await this.getNodeById(targetId);
|
||||||
|
if (sourceNode && targetNode) {
|
||||||
|
const queryResult: RelationQueryResult = {
|
||||||
|
sourceId: sourceId,
|
||||||
|
targetId: targetId,
|
||||||
|
sourceName: sourceNode.name,
|
||||||
|
targetName: targetNode.name,
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||||
|
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||||
|
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||||
|
depth: 0
|
||||||
|
};
|
||||||
|
results.push(queryResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeOrphanNodes(): Promise<number> {
|
||||||
|
if (!this.store) return 0;
|
||||||
|
let deleted = 0;
|
||||||
|
// 优化:批量查询所有有关系的节点 ID,避免 O(N²) 逐节点检查
|
||||||
|
const activeRelPred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
activeRelPred.equalTo('status', 'active');
|
||||||
|
const relResultSet: relationalStore.ResultSet = await this.store.query(activeRelPred, ['subject_id', 'object_id']);
|
||||||
|
const relatedIds = new Set<number>();
|
||||||
|
while (relResultSet.goToNextRow()) {
|
||||||
|
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('subject_id')));
|
||||||
|
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('object_id')));
|
||||||
|
}
|
||||||
|
relResultSet.close();
|
||||||
|
|
||||||
|
// 查询所有节点,筛选出不在关系中的孤儿节点
|
||||||
|
const nodePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
const nodeResultSet: relationalStore.ResultSet = await this.store.query(nodePred, ['id']);
|
||||||
|
const orphanIds: number[] = [];
|
||||||
|
while (nodeResultSet.goToNextRow()) {
|
||||||
|
const nodeId = nodeResultSet.getLong(nodeResultSet.getColumnIndex('id'));
|
||||||
|
if (!relatedIds.has(nodeId)) {
|
||||||
|
orphanIds.push(nodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodeResultSet.close();
|
||||||
|
|
||||||
|
// 批量删除孤儿节点
|
||||||
|
for (const orphanId of orphanIds) {
|
||||||
|
const deletePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
deletePred.equalTo('id', orphanId);
|
||||||
|
await this.store.delete(deletePred);
|
||||||
|
deleted++;
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNodeIdByName(name: string): Promise<number> {
|
||||||
|
if (!this.store) return -1;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
predicates.equalTo('name', name);
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
|
||||||
|
if (resultSet.goToNextRow()) {
|
||||||
|
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
|
resultSet.close();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async introspect(): Promise<IntrospectResult> {
|
||||||
|
if (!this.store) return { entity_count: 0, relation_count: 0, message: 'Database not initialized' };
|
||||||
|
const nodePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
const nodeResultSet = await this.store.query(nodePredicates, ['id']);
|
||||||
|
let entityCount = 0;
|
||||||
|
while (nodeResultSet.goToNextRow()) {
|
||||||
|
entityCount++;
|
||||||
|
}
|
||||||
|
nodeResultSet.close();
|
||||||
|
|
||||||
|
const relPredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
relPredicates.equalTo('status', 'active');
|
||||||
|
const relResultSet = await this.store.query(relPredicates, ['id']);
|
||||||
|
let relationCount = 0;
|
||||||
|
while (relResultSet.goToNextRow()) {
|
||||||
|
relationCount++;
|
||||||
|
}
|
||||||
|
relResultSet.close();
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity_count: entityCount,
|
||||||
|
relation_count: relationCount,
|
||||||
|
message: `数据库包含 ${entityCount} 个实体, ${relationCount} 条关系`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async archive(days: number): Promise<ArchiveResult> {
|
||||||
|
if (!this.store) return { archived: 0, message: 'Database not initialized' };
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||||
|
const cutoffStr = cutoffDate.toISOString();
|
||||||
|
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'active').and().lessThan('created_at', cutoffStr);
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'status': 'archived',
|
||||||
|
'updated_at': new Date().toISOString()
|
||||||
|
};
|
||||||
|
const count = await this.store.update(bucket, predicates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
archived: count,
|
||||||
|
message: `归档了 ${count} 条关系`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(dryRun: boolean): Promise<CleanupResult> {
|
||||||
|
if (!this.store) return { cleaned: 0, message: 'Database not initialized' };
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() - 90);
|
||||||
|
const cutoffStr = cutoffDate.toISOString();
|
||||||
|
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
predicates.equalTo('status', 'deleted').and().lessThan('updated_at', cutoffStr);
|
||||||
|
|
||||||
|
let deleted = 0;
|
||||||
|
if (dryRun) {
|
||||||
|
const resultSet = await this.store.query(predicates, ['id']);
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
deleted++;
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
} else {
|
||||||
|
deleted = await this.store.delete(predicates);
|
||||||
|
const orphanCount = await this.removeOrphanNodes();
|
||||||
|
return {
|
||||||
|
cleaned: deleted + orphanCount,
|
||||||
|
deleted_relations: deleted,
|
||||||
|
deleted_orphans: orphanCount,
|
||||||
|
dry_run: false,
|
||||||
|
message: `删除了 ${deleted} 条关系, ${orphanCount} 个孤立实体`
|
||||||
|
} as CleanupResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cleaned: deleted,
|
||||||
|
deleted_relations: deleted,
|
||||||
|
dry_run: true,
|
||||||
|
message: `将删除 ${deleted} 条关系`
|
||||||
|
} as CleanupResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveChatMessage(role: string, content: string, tools?: string, sessionId?: string): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
|
'session_id': sessionId || null,
|
||||||
|
'role': role,
|
||||||
|
'content': content,
|
||||||
|
'tools': tools || null,
|
||||||
|
'created_at': new Date().toISOString()
|
||||||
|
};
|
||||||
|
await this.store.insert('chat_records', bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChatHistory(limit?: number, sessionId?: string): Promise<ChatMessage[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||||
|
if (sessionId) {
|
||||||
|
predicates.equalTo('session_id', sessionId);
|
||||||
|
}
|
||||||
|
predicates.orderByDesc('created_at');
|
||||||
|
if (limit) {
|
||||||
|
predicates.limitAs(limit);
|
||||||
|
}
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['role', 'content', 'session_id']);
|
||||||
|
const messages: ChatMessage[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const msg: ChatMessage = {
|
||||||
|
role: resultSet.getString(resultSet.getColumnIndex('role')),
|
||||||
|
content: resultSet.getString(resultSet.getColumnIndex('content')),
|
||||||
|
session_id: resultSet.getString(resultSet.getColumnIndex('session_id'))
|
||||||
|
};
|
||||||
|
messages.push(msg);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return messages.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearChatHistory(): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||||
|
await this.store.delete(predicates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllNodes(): Promise<NodeData[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
|
const nodes: NodeData[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const node: NodeData = {
|
||||||
|
id: resultSet.getLong(resultSet.getColumnIndex('id')),
|
||||||
|
label: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||||
|
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||||
|
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
|
||||||
|
};
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllEdges(): Promise<EdgeData[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'relation', 'object_id', 'weight']);
|
||||||
|
const edges: EdgeData[] = [];
|
||||||
|
while (resultSet.goToNextRow()) {
|
||||||
|
const edge: EdgeData = {
|
||||||
|
from: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
|
||||||
|
to: resultSet.getLong(resultSet.getColumnIndex('object_id')),
|
||||||
|
label: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
|
weight: resultSet.getDouble(resultSet.getColumnIndex('weight'))
|
||||||
|
};
|
||||||
|
edges.push(edge);
|
||||||
|
}
|
||||||
|
resultSet.close();
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
}
|
||||||
880
common/src/main/ets/service/AIAgentService.ets
Normal file
880
common/src/main/ets/service/AIAgentService.ets
Normal file
@ -0,0 +1,880 @@
|
|||||||
|
/**
|
||||||
|
* AIAgentService - AI Agent 服务层
|
||||||
|
* 管理上下文感知的 AI 对话,注入图数据作为上下文,
|
||||||
|
* 解析 AI 返回中的记忆操作,调用 GraphMemoryService 执行
|
||||||
|
* 参考:main 分支 core/graph_client.py
|
||||||
|
*/
|
||||||
|
import http from '@ohos.net.http';
|
||||||
|
import dataPreferences from '@ohos.data.preferences';
|
||||||
|
import { Context } from '@ohos.abilityAccessCtrl';
|
||||||
|
import { GraphMemoryService, EntityInfo, RelationInfo, TaskInfo, MemoryRecallParams, MemoryCommitParams, MemoryPurgeParams, PurgeCriteriaParams, NewRelationParams, PersonaUpdateParams, TaskCreateParams, TaskSetStateParams, TaskDeleteParams, TaskLinkInfoParams, TaskArchiveParams, TaskQueryParams, TripletInput, PersonaQueryResult, TaskQueryResult, MemoryRecallResult } from './GraphMemoryService';
|
||||||
|
import { TimeRangeParams } from '../model/GraphDatabase';
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentResponse {
|
||||||
|
content: string;
|
||||||
|
toolCalls: ToolCallResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolCallResult {
|
||||||
|
name: string;
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= 工具定义类型 =========
|
||||||
|
|
||||||
|
// Concrete interface for tool property definitions (replaces Record<string, T>)
|
||||||
|
interface ToolPropertiesDefinition {
|
||||||
|
days?: ToolParamProperty;
|
||||||
|
queryIntent?: ToolParamProperty;
|
||||||
|
seedEntities?: ToolParamProperty;
|
||||||
|
depth?: ToolParamProperty;
|
||||||
|
timeRange?: ToolParamProperty;
|
||||||
|
sessionFilter?: ToolParamProperty;
|
||||||
|
triplets?: ToolParamProperty;
|
||||||
|
entityTypes?: ToolParamProperty;
|
||||||
|
sessionId?: ToolParamProperty;
|
||||||
|
turnId?: ToolParamProperty;
|
||||||
|
criteria?: ToolParamProperty;
|
||||||
|
mode?: ToolParamProperty;
|
||||||
|
newRelation?: ToolParamProperty;
|
||||||
|
tone?: ToolParamProperty;
|
||||||
|
style?: ToolParamProperty;
|
||||||
|
personality?: ToolParamProperty;
|
||||||
|
catchphrase?: ToolParamProperty;
|
||||||
|
background?: ToolParamProperty;
|
||||||
|
taskId?: ToolParamProperty;
|
||||||
|
description?: ToolParamProperty;
|
||||||
|
infoNodes?: ToolParamProperty;
|
||||||
|
state?: ToolParamProperty;
|
||||||
|
deleteInfoNodes?: ToolParamProperty;
|
||||||
|
infoNodeNames?: ToolParamProperty;
|
||||||
|
summary?: ToolParamProperty;
|
||||||
|
limit?: ToolParamProperty;
|
||||||
|
stateFilter?: ToolParamProperty;
|
||||||
|
subject?: ToolParamProperty;
|
||||||
|
relation?: ToolParamProperty;
|
||||||
|
object?: ToolParamProperty;
|
||||||
|
confidence?: ToolParamProperty;
|
||||||
|
subjectContains?: ToolParamProperty;
|
||||||
|
relationType?: ToolParamProperty;
|
||||||
|
targetContains?: ToolParamProperty;
|
||||||
|
target?: ToolParamProperty;
|
||||||
|
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<string, string>;
|
||||||
|
found: boolean;
|
||||||
|
entities: EntityInfo[];
|
||||||
|
relations: RelationInfo[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= 服务方法参数类型 =========
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ========= 工具定义辅助函数 =========
|
||||||
|
|
||||||
|
function makeStringProp(description: string): ToolParamProperty {
|
||||||
|
const result: ToolParamProperty = { type: 'string', description: description };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeIntegerProp(description: string): ToolParamProperty {
|
||||||
|
const result: ToolParamProperty = { type: 'integer', description: description };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeObjectProp(description: string, props: ToolPropertiesDefinition, required?: string[]): ToolParamProperty {
|
||||||
|
const param: ToolParamProperty = { type: 'object', description: description };
|
||||||
|
param.properties = props;
|
||||||
|
if (required && required.length > 0) {
|
||||||
|
param.required = required;
|
||||||
|
}
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeArrayProp(description: string, item: ToolParamProperty): ToolParamProperty {
|
||||||
|
const result: ToolParamProperty = { type: 'array', description: description, items: item };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeBoolProp(description: string): ToolParamProperty {
|
||||||
|
const result: ToolParamProperty = { type: 'boolean', description: description };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNumberProp(description: string): ToolParamProperty {
|
||||||
|
const result: ToolParamProperty = { type: 'number', description: description };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeEnumProp(description: string, enumValues: string[]): ToolParamProperty {
|
||||||
|
const result: ToolParamProperty = { type: 'string', description: description, enum: enumValues };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeToolDef(name: string, description: string, properties: ToolPropertiesDefinition, required?: string[]): ToolFunctionDef {
|
||||||
|
const params: ToolParamDecl = { type: 'object', properties: properties };
|
||||||
|
const func: ToolFunctionDecl = { name: name, description: description, parameters: params };
|
||||||
|
const tool: ToolFunctionDef = { type: 'function', function: func };
|
||||||
|
if (required && required.length > 0) {
|
||||||
|
tool.function.parameters.required = required;
|
||||||
|
}
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统提示词 — AI 人设 + 图记忆使用说明
|
||||||
|
*/
|
||||||
|
function buildSystemPrompt(personaContext: string): string {
|
||||||
|
return `你是 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<string, ToolHandlerName> = {
|
||||||
|
'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<AgentResponse> {
|
||||||
|
this.turnCounter++;
|
||||||
|
|
||||||
|
// === 步骤1+2: 获取上下文 ===
|
||||||
|
const personaResult = await this.memoryService.personaQuery();
|
||||||
|
const personaContext: string = personaResult.found ? this.formatPersona(personaResult.persona) : '';
|
||||||
|
|
||||||
|
const recallParams: MemoryRecallParams = {
|
||||||
|
queryIntent: 'TaskNode,工作记忆,任务链',
|
||||||
|
depth: 2
|
||||||
|
};
|
||||||
|
const taskResult = await this.memoryService.memoryRecall(recallParams);
|
||||||
|
|
||||||
|
// === 读取 API 配置 ===
|
||||||
|
const context = 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<string, Object> = JSON.parse(tc.function.arguments);
|
||||||
|
const result: ToolCallResult = await this.executeTool(handlerName, args);
|
||||||
|
toolCalls.push(result);
|
||||||
|
} else {
|
||||||
|
const unknownToolResult: ToolCallResult = {
|
||||||
|
name: tc.function.name,
|
||||||
|
success: false,
|
||||||
|
message: '未知工具'
|
||||||
|
};
|
||||||
|
toolCalls.push(unknownToolResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有 tool_calls 时需要再次请求 AI,带上工具执行结果
|
||||||
|
const followUpSystemMsg: ApiRequestMessage = { role: 'system', content: systemPrompt };
|
||||||
|
const followUpUserMsg: ApiRequestMessage = { role: 'user', content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
|
||||||
|
const followUpAssistantMsg: ApiRequestMessage = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: aiMessage.content || '(已执行记忆操作)',
|
||||||
|
};
|
||||||
|
const toolResultsMessages: ApiRequestMessage[] = [
|
||||||
|
followUpSystemMsg,
|
||||||
|
followUpUserMsg,
|
||||||
|
followUpAssistantMsg,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const tc of aiMessage.tool_calls) {
|
||||||
|
const callResult: ToolCallResult | undefined = toolCalls.find(r => r.name === tc.function.name);
|
||||||
|
const toolResultMsg: string = callResult ? callResult.message : '完成';
|
||||||
|
const toolResultMessage: ApiRequestMessage = {
|
||||||
|
role: 'tool',
|
||||||
|
content: `工具 ${tc.function.name} 执行结果: ${toolResultMsg}`
|
||||||
|
};
|
||||||
|
toolResultsMessages.push(toolResultMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalResponse: ApiResponse = await this.callApi(toolResultsMessages, baseUrl, model, apiKey);
|
||||||
|
if (finalResponse.choices && finalResponse.choices.length > 0) {
|
||||||
|
const content: string = finalResponse.choices[0].message.content || '';
|
||||||
|
const finalResult: AgentResponse = { content, toolCalls };
|
||||||
|
return finalResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通回复(无 tool_calls)
|
||||||
|
const content: string = aiMessage.content || '';
|
||||||
|
const noToolResponse: AgentResponse = { content, toolCalls };
|
||||||
|
return noToolResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noResponse: AgentResponse = {
|
||||||
|
content: 'AI 无响应',
|
||||||
|
toolCalls
|
||||||
|
};
|
||||||
|
return noResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求 DeepSeek API
|
||||||
|
*/
|
||||||
|
private async callApi(
|
||||||
|
messages: ApiRequestMessage[],
|
||||||
|
baseUrl: string,
|
||||||
|
model: string,
|
||||||
|
apiKey: string
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
const httpRequest = http.createHttp();
|
||||||
|
try {
|
||||||
|
const resp = await httpRequest.request(baseUrl + '/chat/completions', {
|
||||||
|
method: http.RequestMethod.POST,
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + apiKey
|
||||||
|
},
|
||||||
|
extraData: {
|
||||||
|
model: model,
|
||||||
|
messages: messages,
|
||||||
|
tools: TOOLS_DEFINITION,
|
||||||
|
tool_choice: 'auto'
|
||||||
|
},
|
||||||
|
expectDataType: http.HttpDataType.OBJECT,
|
||||||
|
readTimeout: 60000
|
||||||
|
});
|
||||||
|
if (resp.responseCode === 200) {
|
||||||
|
return resp.result as ApiResponse;
|
||||||
|
}
|
||||||
|
const errorMsg: string = `API 请求失败: HTTP ${resp.responseCode}`;
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
} finally {
|
||||||
|
httpRequest.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工具调用
|
||||||
|
*/
|
||||||
|
private async executeTool(name: ToolHandlerName, args: Record<string, Object>): Promise<ToolCallResult> {
|
||||||
|
try {
|
||||||
|
switch (name) {
|
||||||
|
case 'memoryRecal': {
|
||||||
|
const recallArgs: MemoryRecallParams = {
|
||||||
|
queryIntent: args.queryIntent as string,
|
||||||
|
seedEntities: args.seedEntities as string[],
|
||||||
|
depth: (args.depth as number) ?? 2,
|
||||||
|
timeRange: args.timeRange as TimeRangeParams,
|
||||||
|
sessionFilter: args.sessionFilter as string
|
||||||
|
};
|
||||||
|
const recallResult = await this.memoryService.memoryRecall(recallArgs);
|
||||||
|
const result: ToolCallResult = {
|
||||||
|
name: 'memory_recall',
|
||||||
|
success: true,
|
||||||
|
message: `找到 ${recallResult.entities.length} 个实体, ${recallResult.relations.length} 条关系`
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'memoryCommit': {
|
||||||
|
const commitParams: MemoryCommitParams = {
|
||||||
|
triplets: args.triplets as TripletInput[],
|
||||||
|
entityTypes: args.entityTypes as Record<string, string>,
|
||||||
|
sessionId: (args.sessionId as string) || this.currentSessionId,
|
||||||
|
turnId: (args.turnId as number) || this.turnCounter
|
||||||
|
};
|
||||||
|
const commitResult = await this.memoryService.memoryCommit(commitParams);
|
||||||
|
const result: ToolCallResult = {
|
||||||
|
name: 'memory_commit',
|
||||||
|
success: true,
|
||||||
|
message: `已写入 ${commitResult.committedCount} 条记忆`
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'memoryPurge': {
|
||||||
|
const purgeArgs: MemoryPurgeParams = {
|
||||||
|
criteria: args.criteria as PurgeCriteriaParams,
|
||||||
|
mode: args.mode as 'soft' | 'hard' | 'supersede',
|
||||||
|
newRelation: args.newRelation as NewRelationParams
|
||||||
|
};
|
||||||
|
const purgeResult = await this.memoryService.memoryPurge(purgeArgs);
|
||||||
|
const result: ToolCallResult = {
|
||||||
|
name: 'memory_purge',
|
||||||
|
success: true,
|
||||||
|
message: purgeResult.message
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
case '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, string>): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
const keys: string[] = Object.keys(persona);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key: string = keys[i];
|
||||||
|
const val: string = persona[key];
|
||||||
|
parts.push(`${key}: ${val}`);
|
||||||
|
}
|
||||||
|
return parts.length > 0 ? parts.join(';') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建上下文注入块
|
||||||
|
*/
|
||||||
|
private buildContextBlock(
|
||||||
|
personaResult: PersonaQueryResult,
|
||||||
|
taskResult: MemoryRecallResult
|
||||||
|
): string {
|
||||||
|
const blocks: string[] = [];
|
||||||
|
|
||||||
|
if (personaResult.found) {
|
||||||
|
blocks.push(`【当前人设】\n${this.formatPersona(personaResult.persona)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskResult.entities.length > 0) {
|
||||||
|
const entitySample: EntityInfo[] = taskResult.entities.slice(0, 5);
|
||||||
|
const entitiesStr: string = JSON.stringify(entitySample);
|
||||||
|
blocks.push(`【工作记忆】\n${taskResult.message}\n${entitiesStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks.length > 0 ? blocks.join('\n\n') : '【新对话】';
|
||||||
|
}
|
||||||
|
}
|
||||||
1067
common/src/main/ets/service/GraphMemoryService.ets
Normal file
1067
common/src/main/ets/service/GraphMemoryService.ets
Normal file
File diff suppressed because it is too large
Load Diff
13
common/src/main/module.json5
Normal file
13
common/src/main/module.json5
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "common",
|
||||||
|
"type": "shared",
|
||||||
|
"description": "TrulyMEM common shared module",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone",
|
||||||
|
"tablet",
|
||||||
|
"2in1"
|
||||||
|
],
|
||||||
|
"deliveryWithInstall": true
|
||||||
|
}
|
||||||
|
}
|
||||||
1
features/chat/Index.ets
Normal file
1
features/chat/Index.ets
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ChatPage } from './src/main/ets/pages/ChatPage';
|
||||||
8
features/chat/build-profile.json5
Normal file
8
features/chat/build-profile.json5
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
features/chat/oh-package.json5
Normal file
11
features/chat/oh-package.json5
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/chat",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM chat feature module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
171
features/chat/src/main/ets/pages/ChatPage.ets
Normal file
171
features/chat/src/main/ets/pages/ChatPage.ets
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { GraphDatabase } from '@ohos/common';
|
||||||
|
import { GraphMemoryService } from '@ohos/common';
|
||||||
|
import { AIAgentService, ChatMessage, AgentResponse } from '@ohos/common';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct ChatPage {
|
||||||
|
@State messages: ChatMessage[] = [];
|
||||||
|
@State inputText: string = '';
|
||||||
|
@Prop db: GraphDatabase;
|
||||||
|
@State toolCallLog: string = '';
|
||||||
|
@State isThinking: boolean = false;
|
||||||
|
private scrollController: Scroller = new Scroller();
|
||||||
|
private agentService?: AIAgentService;
|
||||||
|
|
||||||
|
async aboutToAppear() {
|
||||||
|
// 初始化图记忆服务和 Agent
|
||||||
|
const memoryService = new GraphMemoryService(this.db);
|
||||||
|
this.agentService = new AIAgentService(memoryService, getContext(this));
|
||||||
|
|
||||||
|
// 加载历史消息(兼容旧数据:无 session_id 时加载全部)
|
||||||
|
const rawHistory = await this.db.getChatHistory(50, this.agentService.getSessionId());
|
||||||
|
if (rawHistory.length === 0) {
|
||||||
|
// 新 session,尝试加载旧消息
|
||||||
|
const legacyHistory = await this.db.getChatHistory(50);
|
||||||
|
this.messages = legacyHistory.map(m => {
|
||||||
|
const msg: ChatMessage = { role: m.role, content: m.content };
|
||||||
|
return msg;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.messages = rawHistory.map(m => {
|
||||||
|
const msg: ChatMessage = { role: m.role, content: m.content };
|
||||||
|
return msg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage() {
|
||||||
|
if (!this.inputText.trim() || !this.agentService) return;
|
||||||
|
|
||||||
|
const userMessage: string = this.inputText;
|
||||||
|
this.inputText = '';
|
||||||
|
|
||||||
|
// 添加用户消息
|
||||||
|
await this.db.saveChatMessage('user', userMessage, '', this.agentService.getSessionId());
|
||||||
|
this.messages = [...this.messages, { role: 'user', content: userMessage }];
|
||||||
|
|
||||||
|
// 显示 loading
|
||||||
|
this.isThinking = true;
|
||||||
|
this.toolCallLog = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 通过 Agent 发送消息
|
||||||
|
const agentResponse: AgentResponse = await this.agentService.sendMessage(userMessage);
|
||||||
|
|
||||||
|
// 记录工具调用日志
|
||||||
|
if (agentResponse.toolCalls.length > 0) {
|
||||||
|
const logs: string[] = agentResponse.toolCalls.map(tc => `🛠 ${tc.name}: ${tc.message}`);
|
||||||
|
this.toolCallLog = logs.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存并显示 AI 回复
|
||||||
|
await this.db.saveChatMessage('assistant', agentResponse.content, this.toolCallLog, this.agentService.getSessionId());
|
||||||
|
this.messages = [...this.messages, { role: 'assistant', content: agentResponse.content }];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Agent request failed: ' + JSON.stringify(err));
|
||||||
|
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${err.message || JSON.stringify(err)}` }];
|
||||||
|
} finally {
|
||||||
|
this.isThinking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column() {
|
||||||
|
// 聊天列表
|
||||||
|
List() {
|
||||||
|
ForEach(this.messages, (msg: ChatMessage) => {
|
||||||
|
ListItem() {
|
||||||
|
Column() {
|
||||||
|
// 角色标识
|
||||||
|
Text(msg.role === 'user' ? '🧑 你' : '🤖 AI')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor(msg.role === 'user' ? '#7C4DFF' : '#999')
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
// 消息内容
|
||||||
|
Text(msg.content)
|
||||||
|
.fontSize(15)
|
||||||
|
.width('100%')
|
||||||
|
.margin({ top: 4 })
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor(msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.border({ width: 1, color: msg.role === 'user' ? 'rgba(124,77,255,0.3)' : 'rgba(255,255,255,0.1)' })
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ left: 8, right: 8, bottom: 8 })
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// loading 指示
|
||||||
|
if (this.isThinking) {
|
||||||
|
ListItem() {
|
||||||
|
Row() {
|
||||||
|
LoadingProgress()
|
||||||
|
.width(20)
|
||||||
|
.height(20)
|
||||||
|
.margin({ right: 8 })
|
||||||
|
.color('#7C4DFF')
|
||||||
|
Text('AI 思考中...')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#7C4DFF')
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('rgba(124,77,255,0.1)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.2)' })
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ left: 8, right: 8, bottom: 8 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具调用日志
|
||||||
|
if (this.toolCallLog && !this.isThinking) {
|
||||||
|
ListItem() {
|
||||||
|
Text(this.toolCallLog)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor('#FF9800')
|
||||||
|
.backgroundColor('rgba(255,152,0,0.1)')
|
||||||
|
.padding(8)
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(255,152,0,0.2)' })
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ left: 8, right: 8, bottom: 4 })
|
||||||
|
.lineHeight(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.backgroundColor('rgba(0,0,0,0.1)')
|
||||||
|
|
||||||
|
// 输入区
|
||||||
|
Row() {
|
||||||
|
TextArea({ text: this.inputText, placeholder: '输入消息...' })
|
||||||
|
.layoutWeight(1)
|
||||||
|
.onChange((v: string) => { this.inputText = v; })
|
||||||
|
.height(40)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.1)')
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
|
||||||
|
|
||||||
|
Button('发送')
|
||||||
|
.enabled(!this.isThinking)
|
||||||
|
.onClick(() => this.sendMessage())
|
||||||
|
.backgroundColor('#7C4DFF')
|
||||||
|
.borderRadius(8)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding(8)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.05)')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.2)', style: BorderStyle.Solid })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('rgba(26,27,46,0.95)')
|
||||||
|
}
|
||||||
|
}
|
||||||
1
features/commonbusiness/Index.ets
Normal file
1
features/commonbusiness/Index.ets
Normal file
@ -0,0 +1 @@
|
|||||||
|
// Common business module - placeholder for shared business logic
|
||||||
8
features/commonbusiness/build-profile.json5
Normal file
8
features/commonbusiness/build-profile.json5
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
features/commonbusiness/oh-package.json5
Normal file
11
features/commonbusiness/oh-package.json5
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/commonbusiness",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM common business module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
features/graph/Index.ets
Normal file
1
features/graph/Index.ets
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { GraphPage } from './src/main/ets/pages/GraphPage';
|
||||||
8
features/graph/build-profile.json5
Normal file
8
features/graph/build-profile.json5
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
features/graph/oh-package.json5
Normal file
11
features/graph/oh-package.json5
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/graph",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM graph feature module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
313
features/graph/src/main/ets/pages/GraphPage.ets
Normal file
313
features/graph/src/main/ets/pages/GraphPage.ets
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
/**
|
||||||
|
* GraphPage — 记忆星图页面
|
||||||
|
* 使用 WebView 显示 Three.js 3D 图可视化
|
||||||
|
* 通过 javaScriptProxy 与 WebView 双向通信
|
||||||
|
*/
|
||||||
|
import web_webview from '@ohos.web.webview';
|
||||||
|
import { GraphDatabase, RecallEntity } from '@ohos/common';
|
||||||
|
import { GraphMemoryService, ConnectionItem, NodeDetailInfo } from '@ohos/common';
|
||||||
|
|
||||||
|
// ========= 图数据结构定义 =========
|
||||||
|
|
||||||
|
interface GraphNodeItem {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
mentions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphEdgeItem {
|
||||||
|
id: number;
|
||||||
|
source: number;
|
||||||
|
target: number;
|
||||||
|
label: string;
|
||||||
|
relation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeDetailInfo 已从 GraphMemoryService 导入
|
||||||
|
|
||||||
|
// ========= WebView 原生桥接 =========
|
||||||
|
|
||||||
|
class NativeBridge {
|
||||||
|
private controller: web_webview.WebviewController;
|
||||||
|
private onRequestGraphData: () => void;
|
||||||
|
private onNodeClickCallback: (nodeId: number, nodeName: string) => void;
|
||||||
|
private onSearchCallback: (query: string) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
controller: web_webview.WebviewController,
|
||||||
|
onRequestGraphData: () => void,
|
||||||
|
onNodeClickCallback: (nodeId: number, nodeName: string) => void,
|
||||||
|
onSearchCallback: (query: string) => void
|
||||||
|
) {
|
||||||
|
this.controller = controller;
|
||||||
|
this.onRequestGraphData = onRequestGraphData;
|
||||||
|
this.onNodeClickCallback = onNodeClickCallback;
|
||||||
|
this.onSearchCallback = onSearchCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeClick(nodeId: number, nodeName: string): void {
|
||||||
|
console.info('Node clicked: id=' + nodeId + ', name=' + nodeName);
|
||||||
|
if (this.onNodeClickCallback) {
|
||||||
|
this.onNodeClickCallback(nodeId, nodeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(query: string): void {
|
||||||
|
console.info('Search from WebView: ' + query);
|
||||||
|
if (this.onSearchCallback) {
|
||||||
|
this.onSearchCallback(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestGraphData(): void {
|
||||||
|
console.info('requestGraphData called from WebView');
|
||||||
|
if (this.onRequestGraphData) {
|
||||||
|
this.onRequestGraphData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= GraphPage 组件 =========
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct GraphPage {
|
||||||
|
private controller: web_webview.WebviewController = new web_webview.WebviewController();
|
||||||
|
@Prop db: GraphDatabase;
|
||||||
|
@State nodeCount: number = 0;
|
||||||
|
@State edgeCount: number = 0;
|
||||||
|
@State selectedNodeDetail: NodeDetailInfo | null = null;
|
||||||
|
@State showNodeDetail: boolean = false;
|
||||||
|
@State searchText: string = '';
|
||||||
|
private graphService: GraphMemoryService = new GraphMemoryService(this.db);
|
||||||
|
|
||||||
|
// 初始化桥接对象
|
||||||
|
private bridge: NativeBridge = new NativeBridge(
|
||||||
|
this.controller,
|
||||||
|
(): void => { this.pushGraphDataToWebView(); },
|
||||||
|
(nodeId: number, nodeName: string): void => { this.handleNodeClick(nodeId, nodeName); },
|
||||||
|
(query: string): void => { this.handleSearchFromWeb(query); }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部触发刷新图数据(聊天写入新记忆后调用)
|
||||||
|
*/
|
||||||
|
public async refreshGraphData(): Promise<void> {
|
||||||
|
await this.pushGraphDataToWebView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理节点点击 - 查询详细信息并显示浮层
|
||||||
|
*/
|
||||||
|
private async handleNodeClick(nodeId: number, nodeName: string): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
try {
|
||||||
|
// 获取所有节点
|
||||||
|
const allNodes: GraphNodeItem[] = await this.getAllNodesData();
|
||||||
|
// 获取所有活跃关系
|
||||||
|
const allEdges: GraphEdgeItem[] = await this.getAllEdgesData();
|
||||||
|
|
||||||
|
// 通过 JavaScript Bridge 推送数据
|
||||||
|
if (this.controller) {
|
||||||
|
const jsCode: string =
|
||||||
|
`window.loadGraphData(${JSON.stringify({ nodes: allNodes, edges: allEdges })});`;
|
||||||
|
this.controller.runJavaScript(jsCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodeCount = allNodes.length;
|
||||||
|
this.edgeCount = allEdges.length;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('pushGraphDataToWebView error: ' + JSON.stringify(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库查询所有节点
|
||||||
|
*/
|
||||||
|
private async getAllNodesData(): Promise<GraphNodeItem[]> {
|
||||||
|
const result = await this.db.search('');
|
||||||
|
const items: GraphNodeItem[] = result.map((r, idx): GraphNodeItem => {
|
||||||
|
const item: GraphNodeItem = {
|
||||||
|
id: idx + 1,
|
||||||
|
label: r.name,
|
||||||
|
type: r.type,
|
||||||
|
mentions: r.mentions
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库查询所有活跃关系
|
||||||
|
*/
|
||||||
|
private async getAllEdgesData(): Promise<GraphEdgeItem[]> {
|
||||||
|
const recallResult = await this.db.recall('', [], 3);
|
||||||
|
const nameToId: Record<string, number> = {};
|
||||||
|
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
|
||||||
|
nameToId[e.name as string] = idx + 1;
|
||||||
|
});
|
||||||
|
const edgeItems: GraphEdgeItem[] = [];
|
||||||
|
for (let i = 0; i < recallResult.relations.length; i++) {
|
||||||
|
const r = recallResult.relations[i];
|
||||||
|
const sourceId: number | undefined = nameToId[r.source];
|
||||||
|
const targetId: number | undefined = nameToId[r.target];
|
||||||
|
if (sourceId !== undefined && targetId !== undefined) {
|
||||||
|
const edgeItem: GraphEdgeItem = {
|
||||||
|
id: i + 1,
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
label: r.type,
|
||||||
|
relation: r.type
|
||||||
|
};
|
||||||
|
edgeItems.push(edgeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgeItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
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%')
|
||||||
|
}
|
||||||
|
}
|
||||||
1
features/settings/Index.ets
Normal file
1
features/settings/Index.ets
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { SettingsPage } from './src/main/ets/pages/SettingsPage';
|
||||||
8
features/settings/build-profile.json5
Normal file
8
features/settings/build-profile.json5
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
features/settings/oh-package.json5
Normal file
11
features/settings/oh-package.json5
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/settings",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM settings feature module",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
126
features/settings/src/main/ets/pages/SettingsPage.ets
Normal file
126
features/settings/src/main/ets/pages/SettingsPage.ets
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import dataPreferences from '@ohos.data.preferences';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export struct SettingsPage {
|
||||||
|
@State baseUrl: string = '';
|
||||||
|
@State model: string = '';
|
||||||
|
@State apiKey: string = '';
|
||||||
|
private pref?: dataPreferences.Preferences;
|
||||||
|
|
||||||
|
async aboutToAppear() {
|
||||||
|
const ctx = getContext(this);
|
||||||
|
this.pref = await dataPreferences.getPreferences(ctx, 'trulymem_config');
|
||||||
|
this.baseUrl = String(await this.pref.get('base_url', 'https://api.deepseek.com'));
|
||||||
|
this.model = String(await this.pref.get('model', 'deepseek-chat'));
|
||||||
|
this.apiKey = String(await this.pref.get('api_key', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBaseUrlChange(value: string) {
|
||||||
|
this.baseUrl = value;
|
||||||
|
await this.pref?.put('base_url', value);
|
||||||
|
await this.pref?.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModelChange(value: string) {
|
||||||
|
this.model = value;
|
||||||
|
await this.pref?.put('model', value);
|
||||||
|
await this.pref?.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onApiKeyChange(value: string) {
|
||||||
|
this.apiKey = value;
|
||||||
|
await this.pref?.put('api_key', value);
|
||||||
|
await this.pref?.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
// 主题色光晕背景
|
||||||
|
Column()
|
||||||
|
.width(200)
|
||||||
|
.height(200)
|
||||||
|
.backgroundColor('rgba(124,77,255,0.15)')
|
||||||
|
.blur(40)
|
||||||
|
.borderRadius(100)
|
||||||
|
.position({ x: '10%', y: '20%' })
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
Text('API 配置')
|
||||||
|
.fontSize(24)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.margin({ top: 20, bottom: 16 })
|
||||||
|
|
||||||
|
// Base URL 设置项
|
||||||
|
Column() {
|
||||||
|
Text('Base URL')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.width('100%')
|
||||||
|
.margin({ bottom: 8 })
|
||||||
|
|
||||||
|
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
|
||||||
|
.onChange((v: string) => { this.onBaseUrlChange(v); })
|
||||||
|
.backgroundColor('rgba(255,255,255,0.1)')
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
|
||||||
|
.height(40)
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.05)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ bottom: 12 })
|
||||||
|
|
||||||
|
// Model ID 设置项
|
||||||
|
Column() {
|
||||||
|
Text('Model ID')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.width('100%')
|
||||||
|
.margin({ bottom: 8 })
|
||||||
|
|
||||||
|
TextInput({ placeholder: 'deepseek-chat', text: this.model })
|
||||||
|
.onChange((v: string) => { this.onModelChange(v); })
|
||||||
|
.backgroundColor('rgba(255,255,255,0.1)')
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
|
||||||
|
.height(40)
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.05)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ bottom: 12 })
|
||||||
|
|
||||||
|
// API Key 设置项
|
||||||
|
Column() {
|
||||||
|
Text('API Key')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.width('100%')
|
||||||
|
.margin({ bottom: 8 })
|
||||||
|
|
||||||
|
TextInput({ placeholder: 'sk-...', text: this.apiKey })
|
||||||
|
.type(InputType.Password)
|
||||||
|
.onChange((v: string) => { this.onApiKeyChange(v); })
|
||||||
|
.backgroundColor('rgba(255,255,255,0.1)')
|
||||||
|
.borderRadius(8)
|
||||||
|
.border({ width: 1, color: 'rgba(124,77,255,0.3)' })
|
||||||
|
.height(40)
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('rgba(255,255,255,0.05)')
|
||||||
|
.borderRadius(12)
|
||||||
|
.backgroundBlurStyle(BlurStyle.Thin)
|
||||||
|
.margin({ bottom: 12 })
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('rgba(26,27,46,0.95)')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
products/phone/build-profile.json5
Normal file
8
products/phone/build-profile.json5
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
14
products/phone/oh-package.json5
Normal file
14
products/phone/oh-package.json5
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohos/phone",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TrulyMEM phone product entry",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common",
|
||||||
|
"@ohos/graph": "file:../../features/graph",
|
||||||
|
"@ohos/chat": "file:../../features/chat",
|
||||||
|
"@ohos/settings": "file:../../features/settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
products/phone/src/main/ets/entryability/EntryAbility.ets
Normal file
35
products/phone/src/main/ets/entryability/EntryAbility.ets
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import UIAbility from '@ohos.app.ability.UIAbility';
|
||||||
|
import window from '@ohos.window';
|
||||||
|
|
||||||
|
export default class EntryAbility extends UIAbility {
|
||||||
|
onCreate(want, launchParam) {
|
||||||
|
console.info('EntryAbility onCreate');
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
console.info('EntryAbility onDestroy');
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowStageCreate(windowStage: window.WindowStage) {
|
||||||
|
console.info('EntryAbility onWindowStageCreate');
|
||||||
|
windowStage.loadContent('pages/Index', (err, data) => {
|
||||||
|
if (err.code) {
|
||||||
|
console.error('Failed to load the content. Cause: ' + JSON.stringify(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.info('Succeeded in loading the content. Data: ' + JSON.stringify(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowStageDestroy() {
|
||||||
|
console.info('EntryAbility onWindowStageDestroy');
|
||||||
|
}
|
||||||
|
|
||||||
|
onForeground() {
|
||||||
|
console.info('EntryAbility onForeground');
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackground() {
|
||||||
|
console.info('EntryAbility onBackground');
|
||||||
|
}
|
||||||
|
}
|
||||||
125
products/phone/src/main/ets/pages/Index.ets
Normal file
125
products/phone/src/main/ets/pages/Index.ets
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { GraphDatabase } from '@ohos/common';
|
||||||
|
import { GraphPage } from '@ohos/graph';
|
||||||
|
import { ChatPage } from '@ohos/chat';
|
||||||
|
import { SettingsPage } from '@ohos/settings';
|
||||||
|
import { ImmersiveTabNavigation } from '@ohos/common';
|
||||||
|
import display from '@ohos.display';
|
||||||
|
|
||||||
|
@Entry
|
||||||
|
@Component
|
||||||
|
struct Index {
|
||||||
|
@State currentIndex: number = 0;
|
||||||
|
private db: GraphDatabase = new GraphDatabase();
|
||||||
|
private displayCallback?: Callback<number>;
|
||||||
|
|
||||||
|
aboutToAppear() {
|
||||||
|
this.db.init(getContext(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutToDisappear() {
|
||||||
|
if (this.displayCallback) {
|
||||||
|
display.off('change', this.displayCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
tabContentBuilder() {
|
||||||
|
Column() {
|
||||||
|
if (this.currentIndex === 0) {
|
||||||
|
MainPage({ db: this.db })
|
||||||
|
} else {
|
||||||
|
SettingsPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
ImmersiveTabNavigation({
|
||||||
|
currentIndex: this.currentIndex,
|
||||||
|
onTabChange: (index: number): void => { this.currentIndex = index; },
|
||||||
|
contentBuilder: (): void => { this.tabContentBuilder(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
struct MainPage {
|
||||||
|
@Prop db: GraphDatabase;
|
||||||
|
@State isWide: boolean = false;
|
||||||
|
|
||||||
|
aboutToAppear() {
|
||||||
|
this.updateBreakpoint();
|
||||||
|
try {
|
||||||
|
display.on('change', () => {
|
||||||
|
this.updateBreakpoint();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('display.on error: ' + JSON.stringify(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBreakpoint(): void {
|
||||||
|
try {
|
||||||
|
const defaultWindow = display.getDefaultDisplaySync();
|
||||||
|
this.isWide = defaultWindow.width > 520;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('updateBreakpoint error: ' + JSON.stringify(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
if (this.isWide) {
|
||||||
|
Row() {
|
||||||
|
GraphPage({ db: this.db })
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height('100%')
|
||||||
|
.clip(true)
|
||||||
|
.borderRadius(12)
|
||||||
|
.margin({ left: 4, right: 2 })
|
||||||
|
|
||||||
|
ChatPage({ db: this.db })
|
||||||
|
.width(380)
|
||||||
|
.height('100%')
|
||||||
|
.clip(true)
|
||||||
|
.borderRadius(12)
|
||||||
|
.margin({ left: 2, right: 4 })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(4)
|
||||||
|
.backgroundColor('#1A1B2E')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
} else {
|
||||||
|
Column() {
|
||||||
|
Stack() {
|
||||||
|
GraphPage({ db: this.db })
|
||||||
|
}
|
||||||
|
.height('55%')
|
||||||
|
.width('100%')
|
||||||
|
.clip(true)
|
||||||
|
.borderRadius(12)
|
||||||
|
.margin({ top: 2, left: 4, right: 4, bottom: 2 })
|
||||||
|
|
||||||
|
Stack() {
|
||||||
|
ChatPage({ db: this.db })
|
||||||
|
}
|
||||||
|
.height('45%')
|
||||||
|
.width('100%')
|
||||||
|
.clip(true)
|
||||||
|
.borderRadius(12)
|
||||||
|
.margin({ top: 2, left: 4, right: 4, bottom: 2 })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(2)
|
||||||
|
.backgroundColor('#1A1B2E')
|
||||||
|
.backgroundBlurStyle(BlurStyle.Regular)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
products/phone/src/main/module.json5
Normal file
46
products/phone/src/main/module.json5
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "phone",
|
||||||
|
"type": "entry",
|
||||||
|
"description": "TrulyMEM phone entry module",
|
||||||
|
"mainElement": "EntryAbility",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone",
|
||||||
|
"tablet",
|
||||||
|
"2in1"
|
||||||
|
],
|
||||||
|
"deliveryWithInstall": true,
|
||||||
|
"installationFree": false,
|
||||||
|
"pages": "$profile:main_pages",
|
||||||
|
"abilities": [
|
||||||
|
{
|
||||||
|
"name": "EntryAbility",
|
||||||
|
"srcEntry": "./ets/entryability/EntryAbility.ets",
|
||||||
|
"description": "$string:EntryAbility_desc",
|
||||||
|
"icon": "$media:layered_image",
|
||||||
|
"label": "$string:EntryAbility_label",
|
||||||
|
"startWindowIcon": "$media:startIcon",
|
||||||
|
"startWindowBackground": "$color:start_window_background",
|
||||||
|
"exported": true,
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"entities": [
|
||||||
|
"entity.system.home"
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
"ohos.want.action.home"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestPermissions": [
|
||||||
|
{
|
||||||
|
"name": "ohos.permission.INTERNET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ohos.permission.GET_NETWORK_INFO"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"src": [
|
||||||
|
"pages/Index"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user