mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-09-21 09:28:19 +00:00
refactor: 工程标准化 — 组件提取、废弃API清理、console清零
- ChatPage/GraphPage/SettingsPage 拆分为独立子组件 - MainPage 接入 ImmersiveTabNavigation 玻璃拟态导航 - EntryAbility console.* 全替换为 defaultLogger - SplashPage replaceUrl 废弃 API 迁移到 UIContext Router - common/Index.ets 双通道导出 Logger/defaultLogger - 删除 dead code (entry/, commonbusiness/, trulymem-core/) - 全工程 0 console.*,BUILD SUCCESSFUL
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
{
|
||||
"app": {
|
||||
"signingConfigs": [],
|
||||
"products": [
|
||||
{
|
||||
"name": "default",
|
||||
@ -36,18 +37,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "commonbusiness",
|
||||
"srcPath": "./features/commonbusiness",
|
||||
"targets": [
|
||||
{
|
||||
"name": "default",
|
||||
"applyToProducts": [
|
||||
"default"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "graph",
|
||||
"srcPath": "./features/graph",
|
||||
@ -96,17 +86,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "trulymem-core",
|
||||
"srcPath": "./trulymem-core",
|
||||
"targets": [
|
||||
{
|
||||
"name": "default",
|
||||
"applyToProducts": [
|
||||
"default"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,7 +1,24 @@
|
||||
export { TrulyMEMConstants } from './src/main/ets/constant/TrulyMEMConstants';
|
||||
export { GraphDatabase, RecallEntity, TimeRangeParams } from './src/main/ets/model/GraphDatabase';
|
||||
export { GraphMemoryService } from './src/main/ets/service/GraphMemoryService';
|
||||
export { AIAgentService, ChatMessage, AgentResponse } from './src/main/ets/service/AIAgentService';
|
||||
export { PageContext, RouterParam, IPageContext } from './src/main/ets/routermanager/PageContext';
|
||||
export { BreakpointType, BreakpointTypes, WidthBreakpoint } from './src/main/ets/util/BreakpointSystem';
|
||||
export { BaseViewModel, VMEvent } from './src/main/ets/viewmodel/BaseViewModel';
|
||||
// ========= Utility Layer =========
|
||||
export { defaultLogger } from './src/main/ets/util/Logger';
|
||||
export { defaultLogger as Logger } from './src/main/ets/util/Logger';
|
||||
|
||||
export { BreakpointType, BreakpointTypes, WidthBreakpoint } from "./src/main/ets/util/BreakpointSystem";
|
||||
|
||||
// ========= Router =========
|
||||
export { PageContext, RouterParam, IPageContext } from "./src/main/ets/routermanager/PageContext";
|
||||
|
||||
// ========= Constants =========
|
||||
export { Constants as TrulyMEMConstants } from "./src/main/ets/constant/TrulyMEMConstants";
|
||||
|
||||
// ========= Model Layer =========
|
||||
export { GraphDatabase, RecallEntity, TimeRangeParams } from "./src/main/ets/model/GraphDatabase";
|
||||
|
||||
// ========= Service Layer =========
|
||||
export { GraphMemoryService, ConnectionItem, NodeDetailInfo } from "./src/main/ets/service/GraphMemoryService";
|
||||
export { AIAgentService, ChatMessage, AgentResponse } from "./src/main/ets/service/AIAgentService";
|
||||
|
||||
// ========= ViewModel Layer =========
|
||||
export { BaseViewModel, VMEvent } from "./src/main/ets/viewmodel/BaseViewModel";
|
||||
|
||||
// ========= Component Layer =========
|
||||
export { ImmersiveTabNavigation } from "./src/main/ets/component/ImmersiveTabNavigation";
|
||||
|
||||
1
common/common
Symbolic link
1
common/common
Symbolic link
@ -0,0 +1 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/common
|
||||
6
common/hvigorfile.ts
Normal file
6
common/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import { defaultLogger } from '../util/Logger';
|
||||
import { window } from '@kit.ArkUI';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
|
||||
@ -19,7 +20,7 @@ export struct ImmersiveTabNavigation {
|
||||
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
|
||||
this.bottomAvoidHeight = avoidArea.bottomRect.height || 0;
|
||||
} catch (e) {
|
||||
console.error('Failed to get avoid area: ' + (e as BusinessError).message);
|
||||
defaultLogger.error('Failed to get avoid area: ' + (e as BusinessError).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,11 @@ interface CriteriaData {
|
||||
target?: string;
|
||||
relation?: string;
|
||||
sessionId?: string;
|
||||
sourceType?: string;
|
||||
targetType?: string;
|
||||
sourceHasStatus?: string;
|
||||
subjectContains?: string;
|
||||
targetContains?: string;
|
||||
}
|
||||
|
||||
interface NodeData {
|
||||
@ -116,6 +121,20 @@ interface SearchResultItem {
|
||||
mentions: number;
|
||||
}
|
||||
|
||||
class TaskNodeRow {
|
||||
id: number = 0;
|
||||
name: string = '';
|
||||
updatedAt: string = '';
|
||||
}
|
||||
|
||||
interface DbTaskInfo {
|
||||
taskId: string;
|
||||
description: string;
|
||||
state: string;
|
||||
infoCount: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
@ -529,12 +548,10 @@ export class GraphDatabase {
|
||||
|
||||
async purge(criteria: CriteriaData, mode: string = 'soft'): Promise<void> {
|
||||
if (!this.store) return;
|
||||
if (!criteria.subject && !criteria.target && !criteria.relation && !criteria.sessionId) {
|
||||
return;
|
||||
}
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
let hasCondition = false;
|
||||
|
||||
// 精确匹配
|
||||
if (criteria.subject) {
|
||||
const subjectId = await this.getNodeIdByName(criteria.subject);
|
||||
if (subjectId > 0) {
|
||||
@ -567,6 +584,96 @@ export class GraphDatabase {
|
||||
hasCondition = true;
|
||||
}
|
||||
|
||||
// 模糊匹配:subjectContains -> 通过子查询匹配节点名
|
||||
if (criteria.subjectContains) {
|
||||
const nodeSql = `SELECT id FROM nodes WHERE name LIKE '%' || ? || '%'`;
|
||||
const nodeResult = await this.store.querySql(nodeSql, [criteria.subjectContains]);
|
||||
const nodeIds: number[] = [];
|
||||
while (nodeResult.goToNextRow()) {
|
||||
nodeIds.push(nodeResult.getLong(0));
|
||||
}
|
||||
nodeResult.close();
|
||||
if (nodeIds.length > 0) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.in('subject_id', nodeIds);
|
||||
hasCondition = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 模糊匹配:targetContains
|
||||
if (criteria.targetContains) {
|
||||
const nodeSql = `SELECT id FROM nodes WHERE name LIKE '%' || ? || '%'`;
|
||||
const nodeResult = await this.store.querySql(nodeSql, [criteria.targetContains]);
|
||||
const nodeIds: number[] = [];
|
||||
while (nodeResult.goToNextRow()) {
|
||||
nodeIds.push(nodeResult.getLong(0));
|
||||
}
|
||||
nodeResult.close();
|
||||
if (nodeIds.length > 0) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.in('object_id', nodeIds);
|
||||
hasCondition = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 源实体类型过滤
|
||||
if (criteria.sourceType) {
|
||||
const nodeSql = `SELECT id FROM nodes WHERE type = ?`;
|
||||
const nodeResult = await this.store.querySql(nodeSql, [criteria.sourceType]);
|
||||
const nodeIds: number[] = [];
|
||||
while (nodeResult.goToNextRow()) {
|
||||
nodeIds.push(nodeResult.getLong(0));
|
||||
}
|
||||
nodeResult.close();
|
||||
if (nodeIds.length > 0) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.in('subject_id', nodeIds);
|
||||
hasCondition = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 目标实体类型过滤
|
||||
if (criteria.targetType) {
|
||||
const nodeSql = `SELECT id FROM nodes WHERE type = ?`;
|
||||
const nodeResult = await this.store.querySql(nodeSql, [criteria.targetType]);
|
||||
const nodeIds: number[] = [];
|
||||
while (nodeResult.goToNextRow()) {
|
||||
nodeIds.push(nodeResult.getLong(0));
|
||||
}
|
||||
nodeResult.close();
|
||||
if (nodeIds.length > 0) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.in('object_id', nodeIds);
|
||||
hasCondition = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 源实体状态过滤
|
||||
if (criteria.sourceHasStatus) {
|
||||
const nodeSql = `SELECT id FROM nodes WHERE type LIKE '%' || ? || '%'`;
|
||||
const nodeResult = await this.store.querySql(nodeSql, [criteria.sourceHasStatus]);
|
||||
const nodeIds: number[] = [];
|
||||
while (nodeResult.goToNextRow()) {
|
||||
nodeIds.push(nodeResult.getLong(0));
|
||||
}
|
||||
nodeResult.close();
|
||||
if (nodeIds.length > 0) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.in('subject_id', nodeIds);
|
||||
hasCondition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCondition) {
|
||||
if (mode === 'soft') {
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
@ -687,43 +794,84 @@ export class GraphDatabase {
|
||||
*/
|
||||
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
|
||||
if (!this.store) return [];
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('status', 'archived');
|
||||
if (keyword) {
|
||||
predicates.and().like('relation', '%' + keyword + '%');
|
||||
}
|
||||
let sql = `
|
||||
SELECT r.subject_id, r.object_id, r.relation, r.weight, r.session_id, r.turn_id,
|
||||
e.name AS source_name, t.name AS target_name
|
||||
FROM relations r
|
||||
JOIN nodes e ON r.subject_id = e.id
|
||||
JOIN nodes t ON r.object_id = t.id
|
||||
WHERE r.status = 'archived'
|
||||
`;
|
||||
const params: string[] = [];
|
||||
if (days && days > 0) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
const maxDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
predicates.and().lessThanOrEqualTo('date_bucket', maxDateBucket);
|
||||
const minDateBucket = cutoff.toISOString().slice(0, 10);
|
||||
sql += ' AND r.date_bucket >= ?';
|
||||
params.push(minDateBucket);
|
||||
}
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||
if (keyword) {
|
||||
const kw = '%' + keyword + '%';
|
||||
sql += ' AND (e.name LIKE ? OR t.name LIKE ? OR r.relation LIKE ?)';
|
||||
params.push(kw, kw, kw);
|
||||
}
|
||||
sql += ' ORDER BY r.updated_at DESC LIMIT 200';
|
||||
const resultSet: relationalStore.ResultSet = await this.store.querySql(sql, params);
|
||||
const results: RelationQueryResult[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||
const sourceNode = await this.getNodeById(sourceId);
|
||||
const targetNode = await this.getNodeById(targetId);
|
||||
if (sourceNode && targetNode) {
|
||||
const queryResult: RelationQueryResult = {
|
||||
sourceId: sourceId,
|
||||
targetId: targetId,
|
||||
sourceName: sourceNode.name,
|
||||
targetName: targetNode.name,
|
||||
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||
depth: 0
|
||||
};
|
||||
results.push(queryResult);
|
||||
}
|
||||
results.push({
|
||||
sourceId: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
|
||||
targetId: resultSet.getLong(resultSet.getColumnIndex('object_id')),
|
||||
sourceName: resultSet.getString(resultSet.getColumnIndex('source_name')),
|
||||
targetName: resultSet.getString(resultSet.getColumnIndex('target_name')),
|
||||
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||
depth: 0
|
||||
});
|
||||
}
|
||||
resultSet.close();
|
||||
return results;
|
||||
}
|
||||
|
||||
async getRecentTasks(limit: number = 10, stateFilter?: string): Promise<DbTaskInfo[]> {
|
||||
if (!this.store) return [];
|
||||
const nodesSql = `SELECT id, name, updated_at FROM nodes WHERE type = 'TaskNode' ORDER BY updated_at DESC LIMIT ?`;
|
||||
const nodesRs: relationalStore.ResultSet = await this.store.querySql(nodesSql, [String(limit)]);
|
||||
const taskNodes: TaskNodeRow[] = [];
|
||||
while (nodesRs.goToNextRow()) {
|
||||
const tn: TaskNodeRow = new TaskNodeRow();
|
||||
tn.id = nodesRs.getLong(nodesRs.getColumnIndex('id'));
|
||||
tn.name = nodesRs.getString(nodesRs.getColumnIndex('name'));
|
||||
tn.updatedAt = nodesRs.getString(nodesRs.getColumnIndex('updated_at')) || '';
|
||||
taskNodes.push(tn);
|
||||
}
|
||||
nodesRs.close();
|
||||
const tasks: DbTaskInfo[] = [];
|
||||
for (const node of taskNodes) {
|
||||
const relSql = `SELECT r.relation, e.name AS target_name FROM relations r JOIN nodes e ON r.object_id = e.id WHERE r.subject_id = ? AND r.status = 'active' AND r.relation IN ('description', 'has_state')`;
|
||||
const relRs: relationalStore.ResultSet = await this.store.querySql(relSql, [String(node.id)]);
|
||||
let description = '';
|
||||
let state = '进行中';
|
||||
while (relRs.goToNextRow()) {
|
||||
const rt: string = relRs.getString(relRs.getColumnIndex('relation'));
|
||||
const targetName: string = relRs.getString(relRs.getColumnIndex('target_name'));
|
||||
if (rt === 'description') description = targetName;
|
||||
if (rt === 'has_state') state = targetName;
|
||||
}
|
||||
relRs.close();
|
||||
if (stateFilter && state !== stateFilter) continue;
|
||||
const cntSql = `SELECT COUNT(*) AS cnt FROM relations WHERE subject_id = ? AND relation = 'CONTAINS_INFO' AND status = 'active'`;
|
||||
const cntRs: relationalStore.ResultSet = await this.store.querySql(cntSql, [String(node.id)]);
|
||||
let infoCount = 0;
|
||||
if (cntRs.goToNextRow()) infoCount = cntRs.getLong(cntRs.getColumnIndex('cnt'));
|
||||
cntRs.close();
|
||||
tasks.push({ taskId: node.name, description, state, infoCount, updatedAt: node.updatedAt });
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private async removeOrphanNodes(): Promise<number> {
|
||||
if (!this.store) return 0;
|
||||
let deleted = 0;
|
||||
|
||||
936
common/src/main/ets/model/GraphDatabase.ets.bak
Normal file
936
common/src/main/ets/model/GraphDatabase.ets.bak
Normal file
@ -0,0 +1,936 @@
|
||||
import relationalStore from '@ohos.data.relationalStore';
|
||||
import { Context } from '@ohos.abilityAccessCtrl';
|
||||
|
||||
interface NodeNameCacheItem { name: string; type: string; mentions: number; }
|
||||
export interface TimeRangeParams { days: number; }
|
||||
|
||||
interface TripletData {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
}
|
||||
|
||||
interface CriteriaData {
|
||||
subject?: string;
|
||||
target?: string;
|
||||
relation?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
interface NodeData {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
interface EdgeData {
|
||||
from: number;
|
||||
to: number;
|
||||
label: string;
|
||||
weight: number;
|
||||
depth?: number;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
}
|
||||
|
||||
interface GraphData {
|
||||
nodes: NodeData[];
|
||||
edges: EdgeData[];
|
||||
}
|
||||
|
||||
export interface RecallEntity {
|
||||
name: string;
|
||||
type: string;
|
||||
mention_count: number;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
interface RecallRelation {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
confidence: number;
|
||||
session_id?: string;
|
||||
turn_id?: number;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
interface RecallResult {
|
||||
entities: RecallEntity[];
|
||||
relations: RecallRelation[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface BfsEntity {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export interface RelationQueryResult {
|
||||
sourceId: number;
|
||||
targetId: number;
|
||||
sourceName: string;
|
||||
targetName: string;
|
||||
type: string;
|
||||
confidence: number;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface NodeQueryResult {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface CleanupResult {
|
||||
cleaned: number;
|
||||
deleted_relations?: number;
|
||||
deleted_orphans?: number;
|
||||
dry_run?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface IntrospectResult {
|
||||
entity_count: number;
|
||||
relation_count: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ArchiveResult {
|
||||
archived: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface SearchResultItem {
|
||||
name: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
interface SnapshotData {
|
||||
entities: RecallEntity[];
|
||||
relations: RecallRelation[];
|
||||
}
|
||||
|
||||
const STORE_CONFIG: relationalStore.StoreConfig = {
|
||||
name: 'trulymem.db',
|
||||
securityLevel: relationalStore.SecurityLevel.S1
|
||||
};
|
||||
|
||||
export class GraphDatabase {
|
||||
private store?: relationalStore.RdbStore;
|
||||
private context?: Context;
|
||||
|
||||
async init(context: Context): Promise<void> {
|
||||
this.context = context;
|
||||
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
|
||||
await this.createTables();
|
||||
}
|
||||
|
||||
private async createTables(): Promise<void> {
|
||||
if (!this.store) return;
|
||||
await this.store.executeSql(`
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT DEFAULT 'concept',
|
||||
mentions INTEGER DEFAULT 1,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
`);
|
||||
await this.store.executeSql(`
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
subject_id INTEGER NOT NULL,
|
||||
relation TEXT NOT NULL,
|
||||
object_id INTEGER NOT NULL,
|
||||
weight REAL DEFAULT 1.0,
|
||||
session_id TEXT,
|
||||
turn_id INTEGER,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
date_bucket TEXT
|
||||
)
|
||||
`);
|
||||
await this.store.executeSql(`
|
||||
CREATE TABLE IF NOT EXISTS chat_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
tools TEXT,
|
||||
created_at TEXT
|
||||
)
|
||||
`);
|
||||
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_name ON nodes(name)`);
|
||||
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_node_type ON nodes(type)`);
|
||||
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_source ON relations(subject_id)`);
|
||||
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_target ON relations(object_id)`);
|
||||
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_type ON relations(relation)`);
|
||||
await this.store.executeSql(`CREATE INDEX IF NOT EXISTS idx_rel_status ON relations(status)`);
|
||||
}
|
||||
|
||||
async commit(triplets: TripletData[], entityTypes?: Record<string, string>, sessionId?: string, turnId?: number): Promise<void> {
|
||||
if (!this.store) return;
|
||||
for (const triplet of triplets) {
|
||||
const subjectId: number = await this.upsertNode(triplet.subject, entityTypes?.[triplet.subject]);
|
||||
const objectId: number = await this.upsertNode(triplet.object, entityTypes?.[triplet.object]);
|
||||
const existingId: number = await this.checkDuplicateRelation(subjectId, triplet.relation, objectId);
|
||||
if (existingId > 0) {
|
||||
continue;
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const dateBucket = now.split('T')[0];
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
'subject_id': subjectId,
|
||||
'relation': triplet.relation,
|
||||
'object_id': objectId,
|
||||
'session_id': sessionId || null,
|
||||
'turn_id': turnId || null,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
'status': 'active',
|
||||
'date_bucket': dateBucket
|
||||
};
|
||||
await this.store.insert('relations', bucket);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkDuplicateRelation(subjectId: number, relation: string, objectId: number): Promise<number> {
|
||||
if (!this.store) return -1;
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('subject_id', subjectId).and().equalTo('relation', relation).and().equalTo('object_id', objectId).and().equalTo('status', 'active');
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
|
||||
if (resultSet.goToFirstRow()) {
|
||||
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||
resultSet.close();
|
||||
return id;
|
||||
}
|
||||
resultSet.close();
|
||||
return -1;
|
||||
}
|
||||
|
||||
private async upsertNode(name: string, entityType?: string): Promise<number> {
|
||||
if (!this.store) return -1;
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
predicates.equalTo('name', name);
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'mentions']);
|
||||
const now = new Date().toISOString();
|
||||
if (resultSet.goToNextRow()) {
|
||||
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||
const mentions: number = resultSet.getLong(resultSet.getColumnIndex('mentions'));
|
||||
resultSet.close();
|
||||
const updatePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
updatePredicates.equalTo('id', id);
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
'mentions': mentions + 1,
|
||||
'updated_at': now
|
||||
};
|
||||
await this.store.update(bucket, updatePredicates);
|
||||
return id;
|
||||
}
|
||||
resultSet.close();
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
'name': name,
|
||||
'type': entityType || 'concept',
|
||||
'mentions': 1,
|
||||
'created_at': now,
|
||||
'updated_at': now
|
||||
};
|
||||
return await this.store.insert('nodes', bucket);
|
||||
}
|
||||
|
||||
async recall(queryIntent: string, seedEntities?: string[], depth: number = 2, timeRange?: TimeRangeParams, sessionFilter?: string): Promise<RecallResult> {
|
||||
if (!this.store) {
|
||||
return { entities: [], relations: [], message: 'Database not initialized' };
|
||||
}
|
||||
// 计算时间范围过滤
|
||||
let minDateBucket: string | undefined;
|
||||
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||
minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
}
|
||||
const keywords = queryIntent.toLowerCase().replace(/,/g, ' ').split(/\s+/).filter(w => w.trim());
|
||||
const allEntities: BfsEntity[] = [];
|
||||
const entityIds = new Set<number>();
|
||||
let seedEntityIds = new Set<number>();
|
||||
|
||||
if (keywords.length === 0 && (!seedEntities || seedEntities.length === 0)) {
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
predicates.orderByDesc('mentions');
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||
while (resultSet.goToNextRow() && allEntities.length < 50) {
|
||||
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||
const name = resultSet.getString(resultSet.getColumnIndex('name'));
|
||||
const type = resultSet.getString(resultSet.getColumnIndex('type'));
|
||||
const mentions = resultSet.getLong(resultSet.getColumnIndex('mentions'));
|
||||
entityIds.add(id);
|
||||
allEntities.push({ id, name, type, mentions, depth: 0 });
|
||||
}
|
||||
resultSet.close();
|
||||
} else {
|
||||
if (seedEntities && seedEntities.length > 0) {
|
||||
for (const seedName of seedEntities) {
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
predicates.equalTo('name', seedName);
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||
while (resultSet.goToNextRow()) {
|
||||
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||
if (!entityIds.has(id)) {
|
||||
entityIds.add(id);
|
||||
seedEntityIds.add(id);
|
||||
allEntities.push({
|
||||
id,
|
||||
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||
depth: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
resultSet.close();
|
||||
}
|
||||
}
|
||||
for (const keyword of keywords) {
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
predicates.like('name', `%${keyword}%`);
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||
while (resultSet.goToNextRow()) {
|
||||
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||
if (!entityIds.has(id)) {
|
||||
entityIds.add(id);
|
||||
allEntities.push({
|
||||
id,
|
||||
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||
depth: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
resultSet.close();
|
||||
}
|
||||
}
|
||||
|
||||
const allRelations: RelationQueryResult[] = [];
|
||||
let currentLayerIds = new Set<number>(entityIds);
|
||||
const visitedEntityIds = new Set<number>(entityIds);
|
||||
// 批量预加载所有相关节点名称,减少 N+1 查询
|
||||
const nodeNameCache = new Map<number, NodeNameCacheItem>();
|
||||
|
||||
for (let layer = 0; layer < depth && currentLayerIds.size > 0; layer++) {
|
||||
const currentIds = Array.from(currentLayerIds);
|
||||
const relations = await this.getRelationsForNodes(currentIds, sessionFilter, minDateBucket);
|
||||
const nextLayerIds = new Set<number>();
|
||||
|
||||
for (const rel of relations) {
|
||||
allRelations.push(rel);
|
||||
if (!visitedEntityIds.has(rel.targetId)) {
|
||||
nextLayerIds.add(rel.targetId);
|
||||
}
|
||||
if (rel.targetId !== rel.sourceId && !visitedEntityIds.has(rel.sourceId)) {
|
||||
nextLayerIds.add(rel.sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const newId of nextLayerIds) {
|
||||
if (!visitedEntityIds.has(newId)) {
|
||||
visitedEntityIds.add(newId);
|
||||
// 优先从缓存获取,避免 N+1 查询
|
||||
const cached = nodeNameCache.get(newId);
|
||||
if (cached) {
|
||||
const addedEntity: BfsEntity = {
|
||||
id: newId,
|
||||
name: cached.name,
|
||||
type: cached.type,
|
||||
mentions: cached.mentions,
|
||||
depth: layer + 1
|
||||
};
|
||||
allEntities.push(addedEntity);
|
||||
} else {
|
||||
const nodeData = await this.getNodeById(newId);
|
||||
if (nodeData) {
|
||||
const cacheItem: NodeNameCacheItem = { name: nodeData.name, type: nodeData.type, mentions: nodeData.mentions };
|
||||
nodeNameCache.set(newId, cacheItem);
|
||||
const addedEntity: BfsEntity = {
|
||||
id: nodeData.id,
|
||||
name: nodeData.name,
|
||||
type: nodeData.type,
|
||||
mentions: nodeData.mentions,
|
||||
depth: layer + 1
|
||||
};
|
||||
allEntities.push(addedEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currentLayerIds = nextLayerIds;
|
||||
}
|
||||
|
||||
const entities: RecallEntity[] = allEntities.map(e => {
|
||||
const entity: RecallEntity = {
|
||||
name: e.name,
|
||||
type: e.type,
|
||||
mention_count: e.mentions,
|
||||
depth: e.depth
|
||||
};
|
||||
return entity;
|
||||
});
|
||||
const relations: RecallRelation[] = allRelations.map(r => {
|
||||
const rel: RecallRelation = {
|
||||
source: r.sourceName,
|
||||
target: r.targetName,
|
||||
type: r.type,
|
||||
confidence: r.confidence,
|
||||
session_id: r.sessionId,
|
||||
turn_id: r.turnId,
|
||||
depth: r.depth
|
||||
};
|
||||
return rel;
|
||||
});
|
||||
|
||||
return {
|
||||
entities,
|
||||
relations,
|
||||
message: `找到 ${entities.length} 个实体, ${relations.length} 条关系`
|
||||
};
|
||||
}
|
||||
|
||||
private async getNodeById(id: number): Promise<NodeQueryResult | null> {
|
||||
if (!this.store) return null;
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
predicates.equalTo('id', id);
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||
if (resultSet.goToNextRow()) {
|
||||
const node: NodeQueryResult = {
|
||||
id: resultSet.getLong(resultSet.getColumnIndex('id')),
|
||||
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions')),
|
||||
depth: 0
|
||||
};
|
||||
resultSet.close();
|
||||
return node;
|
||||
}
|
||||
resultSet.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getRelationsForNodes(nodeIds: number[], sessionFilter?: string, minDateBucket?: string): Promise<RelationQueryResult[]> {
|
||||
if (!this.store || nodeIds.length === 0) return [];
|
||||
const relations: RelationQueryResult[] = [];
|
||||
// 批量预加载所有节点名称到缓存,避免 N+1 查询
|
||||
const nodeNameCache = new Map<number, NodeNameCacheItem>();
|
||||
for (const id of nodeIds) {
|
||||
const node = await this.getNodeById(id);
|
||||
if (node) {
|
||||
const cacheItem: NodeNameCacheItem = { name: node.name, type: node.type, mentions: node.mentions };
|
||||
nodeNameCache.set(id, cacheItem);
|
||||
}
|
||||
}
|
||||
for (const nodeId of nodeIds) {
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('status', 'active').and().equalTo('subject_id', nodeId);
|
||||
if (sessionFilter) {
|
||||
predicates.and().equalTo('session_id', sessionFilter);
|
||||
}
|
||||
if (minDateBucket) {
|
||||
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||
}
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||
while (resultSet.goToNextRow()) {
|
||||
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
|
||||
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
|
||||
if (sourceNode && targetNode) {
|
||||
relations.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
sourceName: sourceNode.name,
|
||||
targetName: targetNode.name,
|
||||
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||
depth: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
resultSet.close();
|
||||
|
||||
const predicates2: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates2.equalTo('status', 'active').and().equalTo('object_id', nodeId);
|
||||
if (sessionFilter) {
|
||||
predicates2.and().equalTo('session_id', sessionFilter);
|
||||
}
|
||||
if (minDateBucket) {
|
||||
predicates2.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||
}
|
||||
const resultSet2: relationalStore.ResultSet = await this.store.query(predicates2, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||
while (resultSet2.goToNextRow()) {
|
||||
const sourceId = resultSet2.getLong(resultSet2.getColumnIndex('subject_id'));
|
||||
const targetId = resultSet2.getLong(resultSet2.getColumnIndex('object_id'));
|
||||
const sourceNode = nodeNameCache.get(sourceId) || await this.getNodeById(sourceId);
|
||||
const targetNode = nodeNameCache.get(targetId) || await this.getNodeById(targetId);
|
||||
if (sourceNode && targetNode) {
|
||||
relations.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
sourceName: sourceNode.name,
|
||||
targetName: targetNode.name,
|
||||
type: resultSet2.getString(resultSet2.getColumnIndex('relation')),
|
||||
confidence: resultSet2.getDouble(resultSet2.getColumnIndex('weight')),
|
||||
sessionId: resultSet2.getString(resultSet2.getColumnIndex('session_id')),
|
||||
turnId: resultSet2.getLong(resultSet2.getColumnIndex('turn_id')),
|
||||
depth: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
resultSet2.close();
|
||||
}
|
||||
return relations;
|
||||
}
|
||||
|
||||
async search(keyword: string): Promise<SearchResultItem[]> {
|
||||
if (!this.store) return [];
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
predicates.like('name', `%${keyword}%`);
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['name', 'type', 'mentions']);
|
||||
const results: SearchResultItem[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
results.push({
|
||||
name: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
|
||||
});
|
||||
}
|
||||
resultSet.close();
|
||||
return results;
|
||||
}
|
||||
|
||||
async purge(criteria: CriteriaData, mode: string = 'soft'): Promise<void> {
|
||||
if (!this.store) return;
|
||||
if (!criteria.subject && !criteria.target && !criteria.relation && !criteria.sessionId) {
|
||||
return;
|
||||
}
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
let hasCondition = false;
|
||||
|
||||
if (criteria.subject) {
|
||||
const subjectId = await this.getNodeIdByName(criteria.subject);
|
||||
if (subjectId > 0) {
|
||||
predicates.equalTo('subject_id', subjectId);
|
||||
hasCondition = true;
|
||||
}
|
||||
}
|
||||
if (criteria.target) {
|
||||
const targetId = await this.getNodeIdByName(criteria.target);
|
||||
if (targetId > 0) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.equalTo('object_id', targetId);
|
||||
hasCondition = true;
|
||||
}
|
||||
}
|
||||
if (criteria.relation) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.equalTo('relation', criteria.relation);
|
||||
hasCondition = true;
|
||||
}
|
||||
if (criteria.sessionId) {
|
||||
if (hasCondition) {
|
||||
predicates.and();
|
||||
}
|
||||
predicates.equalTo('session_id', criteria.sessionId);
|
||||
hasCondition = true;
|
||||
}
|
||||
|
||||
if (hasCondition) {
|
||||
if (mode === 'soft') {
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
'status': 'deleted',
|
||||
'updated_at': new Date().toISOString()
|
||||
};
|
||||
await this.store.update(bucket, predicates);
|
||||
} else {
|
||||
await this.store.delete(predicates);
|
||||
}
|
||||
}
|
||||
await this.removeOrphanNodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 记忆图谱 — 在指定时间范围内查询关系和节点
|
||||
* 对应 tools.memory_graph
|
||||
*/
|
||||
async graph(timeRange: TimeRangeParams, sessionFilter?: string): Promise<GraphData> {
|
||||
if (!this.store) return { nodes: [], edges: [] };
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('status', 'active');
|
||||
if (sessionFilter) {
|
||||
predicates.and().equalTo('session_id', sessionFilter);
|
||||
}
|
||||
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||
}
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||
const nodeIds = new Set<number>();
|
||||
const edges: EdgeData[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||
nodeIds.add(sourceId);
|
||||
nodeIds.add(targetId);
|
||||
const edge: EdgeData = {
|
||||
from: sourceId,
|
||||
to: targetId,
|
||||
label: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||
weight: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
|
||||
};
|
||||
edges.push(edge);
|
||||
}
|
||||
resultSet.close();
|
||||
const nodes: NodeData[] = [];
|
||||
for (const id of nodeIds) {
|
||||
const node = await this.getNodeById(id);
|
||||
if (node) {
|
||||
const nodeData: NodeData = { id: node.id, label: node.name, type: node.type, mentions: node.mentions };
|
||||
nodes.push(nodeData);
|
||||
}
|
||||
}
|
||||
const result: GraphData = { nodes, edges };
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记忆快照 — 在指定时间范围内查询实体和关系
|
||||
* 对应 tools.memory_snapshot
|
||||
*/
|
||||
async snapshot(timeRange: TimeRangeParams, sessionFilter?: string): Promise<SnapshotData> {
|
||||
if (!this.store) return { entities: [], relations: [] };
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('status', 'active');
|
||||
if (sessionFilter) {
|
||||
predicates.and().equalTo('session_id', sessionFilter);
|
||||
}
|
||||
if (timeRange && timeRange.days && timeRange.days > 0) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - timeRange.days);
|
||||
const minDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
predicates.and().greaterThanOrEqualTo('date_bucket', minDateBucket);
|
||||
}
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||
const nodeIds = new Set<number>();
|
||||
const relations: RecallRelation[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||
nodeIds.add(sourceId);
|
||||
nodeIds.add(targetId);
|
||||
const sourceNode = await this.getNodeById(sourceId);
|
||||
const targetNode = await this.getNodeById(targetId);
|
||||
if (sourceNode && targetNode) {
|
||||
const rel: RecallRelation = {
|
||||
source: sourceNode.name,
|
||||
target: targetNode.name,
|
||||
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||
session_id: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||
turn_id: resultSet.getLong(resultSet.getColumnIndex('turn_id'))
|
||||
};
|
||||
relations.push(rel);
|
||||
}
|
||||
}
|
||||
resultSet.close();
|
||||
const entities: RecallEntity[] = [];
|
||||
for (const id of nodeIds) {
|
||||
const node = await this.getNodeById(id);
|
||||
if (node) {
|
||||
const recallEntity: RecallEntity = { name: node.name, type: node.type, mention_count: node.mentions };
|
||||
entities.push(recallEntity);
|
||||
}
|
||||
}
|
||||
const snapResult: SnapshotData = { entities, relations };
|
||||
return snapResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询已归档的记忆
|
||||
* 对应 tools.memory_query_archived
|
||||
*/
|
||||
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
|
||||
if (!this.store) return [];
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('status', 'archived');
|
||||
if (keyword) {
|
||||
predicates.and().like('relation', '%' + keyword + '%');
|
||||
}
|
||||
if (days && days > 0) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
const maxDateBucket = cutoff.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
predicates.and().lessThanOrEqualTo('date_bucket', maxDateBucket);
|
||||
}
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'object_id', 'relation', 'weight', 'session_id', 'turn_id']);
|
||||
const results: RelationQueryResult[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
const sourceId = resultSet.getLong(resultSet.getColumnIndex('subject_id'));
|
||||
const targetId = resultSet.getLong(resultSet.getColumnIndex('object_id'));
|
||||
const sourceNode = await this.getNodeById(sourceId);
|
||||
const targetNode = await this.getNodeById(targetId);
|
||||
if (sourceNode && targetNode) {
|
||||
const queryResult: RelationQueryResult = {
|
||||
sourceId: sourceId,
|
||||
targetId: targetId,
|
||||
sourceName: sourceNode.name,
|
||||
targetName: targetNode.name,
|
||||
type: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||
confidence: resultSet.getDouble(resultSet.getColumnIndex('weight')),
|
||||
sessionId: resultSet.getString(resultSet.getColumnIndex('session_id')),
|
||||
turnId: resultSet.getLong(resultSet.getColumnIndex('turn_id')),
|
||||
depth: 0
|
||||
};
|
||||
results.push(queryResult);
|
||||
}
|
||||
}
|
||||
resultSet.close();
|
||||
return results;
|
||||
}
|
||||
|
||||
private async removeOrphanNodes(): Promise<number> {
|
||||
if (!this.store) return 0;
|
||||
let deleted = 0;
|
||||
// 优化:批量查询所有有关系的节点 ID,避免 O(N²) 逐节点检查
|
||||
const activeRelPred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
activeRelPred.equalTo('status', 'active');
|
||||
const relResultSet: relationalStore.ResultSet = await this.store.query(activeRelPred, ['subject_id', 'object_id']);
|
||||
const relatedIds = new Set<number>();
|
||||
while (relResultSet.goToNextRow()) {
|
||||
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('subject_id')));
|
||||
relatedIds.add(relResultSet.getLong(relResultSet.getColumnIndex('object_id')));
|
||||
}
|
||||
relResultSet.close();
|
||||
|
||||
// 查询所有节点,筛选出不在关系中的孤儿节点
|
||||
const nodePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
const nodeResultSet: relationalStore.ResultSet = await this.store.query(nodePred, ['id']);
|
||||
const orphanIds: number[] = [];
|
||||
while (nodeResultSet.goToNextRow()) {
|
||||
const nodeId = nodeResultSet.getLong(nodeResultSet.getColumnIndex('id'));
|
||||
if (!relatedIds.has(nodeId)) {
|
||||
orphanIds.push(nodeId);
|
||||
}
|
||||
}
|
||||
nodeResultSet.close();
|
||||
|
||||
// 批量删除孤儿节点
|
||||
for (const orphanId of orphanIds) {
|
||||
const deletePred: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
deletePred.equalTo('id', orphanId);
|
||||
await this.store.delete(deletePred);
|
||||
deleted++;
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
private async getNodeIdByName(name: string): Promise<number> {
|
||||
if (!this.store) return -1;
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
predicates.equalTo('name', name);
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
|
||||
if (resultSet.goToNextRow()) {
|
||||
const id = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||
resultSet.close();
|
||||
return id;
|
||||
}
|
||||
resultSet.close();
|
||||
return -1;
|
||||
}
|
||||
|
||||
async introspect(): Promise<IntrospectResult> {
|
||||
if (!this.store) return { entity_count: 0, relation_count: 0, message: 'Database not initialized' };
|
||||
const nodePredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
const nodeResultSet = await this.store.query(nodePredicates, ['id']);
|
||||
let entityCount = 0;
|
||||
while (nodeResultSet.goToNextRow()) {
|
||||
entityCount++;
|
||||
}
|
||||
nodeResultSet.close();
|
||||
|
||||
const relPredicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
relPredicates.equalTo('status', 'active');
|
||||
const relResultSet = await this.store.query(relPredicates, ['id']);
|
||||
let relationCount = 0;
|
||||
while (relResultSet.goToNextRow()) {
|
||||
relationCount++;
|
||||
}
|
||||
relResultSet.close();
|
||||
|
||||
return {
|
||||
entity_count: entityCount,
|
||||
relation_count: relationCount,
|
||||
message: `数据库包含 ${entityCount} 个实体, ${relationCount} 条关系`
|
||||
};
|
||||
}
|
||||
|
||||
async archive(days: number): Promise<ArchiveResult> {
|
||||
if (!this.store) return { archived: 0, message: 'Database not initialized' };
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
const cutoffStr = cutoffDate.toISOString();
|
||||
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('status', 'active').and().lessThan('created_at', cutoffStr);
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
'status': 'archived',
|
||||
'updated_at': new Date().toISOString()
|
||||
};
|
||||
const count = await this.store.update(bucket, predicates);
|
||||
|
||||
return {
|
||||
archived: count,
|
||||
message: `归档了 ${count} 条关系`
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup(dryRun: boolean): Promise<CleanupResult> {
|
||||
if (!this.store) return { cleaned: 0, message: 'Database not initialized' };
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 90);
|
||||
const cutoffStr = cutoffDate.toISOString();
|
||||
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
predicates.equalTo('status', 'deleted').and().lessThan('updated_at', cutoffStr);
|
||||
|
||||
let deleted = 0;
|
||||
if (dryRun) {
|
||||
const resultSet = await this.store.query(predicates, ['id']);
|
||||
while (resultSet.goToNextRow()) {
|
||||
deleted++;
|
||||
}
|
||||
resultSet.close();
|
||||
} else {
|
||||
deleted = await this.store.delete(predicates);
|
||||
const orphanCount = await this.removeOrphanNodes();
|
||||
return {
|
||||
cleaned: deleted + orphanCount,
|
||||
deleted_relations: deleted,
|
||||
deleted_orphans: orphanCount,
|
||||
dry_run: false,
|
||||
message: `删除了 ${deleted} 条关系, ${orphanCount} 个孤立实体`
|
||||
} as CleanupResult;
|
||||
}
|
||||
|
||||
return {
|
||||
cleaned: deleted,
|
||||
deleted_relations: deleted,
|
||||
dry_run: true,
|
||||
message: `将删除 ${deleted} 条关系`
|
||||
} as CleanupResult;
|
||||
}
|
||||
|
||||
async saveChatMessage(role: string, content: string, tools?: string, sessionId?: string): Promise<void> {
|
||||
if (!this.store) return;
|
||||
const bucket: relationalStore.ValuesBucket = {
|
||||
'session_id': sessionId || null,
|
||||
'role': role,
|
||||
'content': content,
|
||||
'tools': tools || null,
|
||||
'created_at': new Date().toISOString()
|
||||
};
|
||||
await this.store.insert('chat_records', bucket);
|
||||
}
|
||||
|
||||
async getChatHistory(limit?: number, sessionId?: string): Promise<ChatMessage[]> {
|
||||
if (!this.store) return [];
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||
if (sessionId) {
|
||||
predicates.equalTo('session_id', sessionId);
|
||||
}
|
||||
predicates.orderByDesc('created_at');
|
||||
if (limit) {
|
||||
predicates.limitAs(limit);
|
||||
}
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['role', 'content', 'session_id']);
|
||||
const messages: ChatMessage[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
const msg: ChatMessage = {
|
||||
role: resultSet.getString(resultSet.getColumnIndex('role')),
|
||||
content: resultSet.getString(resultSet.getColumnIndex('content')),
|
||||
session_id: resultSet.getString(resultSet.getColumnIndex('session_id'))
|
||||
};
|
||||
messages.push(msg);
|
||||
}
|
||||
resultSet.close();
|
||||
return messages.reverse();
|
||||
}
|
||||
|
||||
async clearChatHistory(): Promise<void> {
|
||||
if (!this.store) return;
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||
await this.store.delete(predicates);
|
||||
}
|
||||
|
||||
private async getAllNodes(): Promise<NodeData[]> {
|
||||
if (!this.store) return [];
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||
const nodes: NodeData[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
const node: NodeData = {
|
||||
id: resultSet.getLong(resultSet.getColumnIndex('id')),
|
||||
label: resultSet.getString(resultSet.getColumnIndex('name')),
|
||||
type: resultSet.getString(resultSet.getColumnIndex('type')),
|
||||
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
resultSet.close();
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private async getAllEdges(): Promise<EdgeData[]> {
|
||||
if (!this.store) return [];
|
||||
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'relation', 'object_id', 'weight']);
|
||||
const edges: EdgeData[] = [];
|
||||
while (resultSet.goToNextRow()) {
|
||||
const edge: EdgeData = {
|
||||
from: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
|
||||
to: resultSet.getLong(resultSet.getColumnIndex('object_id')),
|
||||
label: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||
weight: resultSet.getDouble(resultSet.getColumnIndex('weight'))
|
||||
};
|
||||
edges.push(edge);
|
||||
}
|
||||
resultSet.close();
|
||||
return edges;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import { defaultLogger } from '../util/Logger';
|
||||
|
||||
export interface RouterParam {
|
||||
routerName: string;
|
||||
param?: object;
|
||||
@ -24,7 +26,7 @@ export class PageContext implements IPageContext {
|
||||
try {
|
||||
this.pathStack.replacePath({ name: data.routerName, param: data.param }, animated);
|
||||
} catch (err) {
|
||||
console.error('Open Page ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
|
||||
defaultLogger.error('replacePage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +34,7 @@ export class PageContext implements IPageContext {
|
||||
try {
|
||||
this.pathStack.pushPath({ name: data.routerName, param: data.param }, animated);
|
||||
} catch (err) {
|
||||
console.error('Open Page ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
|
||||
defaultLogger.error('openPage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +42,7 @@ export class PageContext implements IPageContext {
|
||||
try {
|
||||
this.pathStack.pop(animated);
|
||||
} catch (err) {
|
||||
console.error('Pop Page failed. ' + err.code + ' ' + err.message);
|
||||
defaultLogger.error('popPage failed. ' + err.code + ' ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -68,6 +68,9 @@ interface ToolPropertiesDefinition {
|
||||
dryRun?: ToolParamProperty;
|
||||
keyword?: ToolParamProperty;
|
||||
attribute?: ToolParamProperty;
|
||||
sourceType?: ToolParamProperty;
|
||||
targetType?: ToolParamProperty;
|
||||
sourceHasStatus?: ToolParamProperty;
|
||||
}
|
||||
|
||||
interface ToolParamProperty {
|
||||
@ -218,19 +221,23 @@ function buildSystemPrompt(personaContext: string): string {
|
||||
## 核心身份
|
||||
${personaContext || '你是一个帮助用户记录和回忆信息的助手。你的核心能力是基于图数据库的记忆系统。'}
|
||||
|
||||
## 记忆系统
|
||||
你的记忆存储在图数据库中,每个记忆都是「实体 - 关系 - 实体」的三元组形式。
|
||||
每轮对话开始,你都会按以下顺序操作:
|
||||
## ⚠️ 内部执行顺序(不得向用户输出)
|
||||
|
||||
### 步骤1:查询人设
|
||||
使用 memory_recall 查询当前人设,确保回复风格一致。
|
||||
以下步骤是内部流程,绝对不要在你的回复中提及或输出。应当仅通过工具调用悄悄完成,回复时直接给出自然的对话内容。
|
||||
|
||||
### 步骤2:查询工作记忆链
|
||||
使用 memory_recall 查询任务链,了解会话上下文和进展中的任务。
|
||||
1. memory_recall → 查询人设图
|
||||
2. task_query → 查询工作记忆链/最近任务
|
||||
3. 处理对话内容 + 思考回复
|
||||
4. memory_commit → 写入本轮关键信息到图数据库
|
||||
5. task_archive → 归档已完成的旧任务
|
||||
6. 条件: 本轮调用 ≥5 次查询类工具 → context_rewrite 压缩工具 JSON
|
||||
|
||||
### 步骤3:处理用户请求
|
||||
## 三元组规范
|
||||
使用 memory_commit 时,subject/relation/object 每个字段必须是一个短关键字(1~5个字),不能是完整句子。
|
||||
|
||||
### 步骤4:更新工作记忆链
|
||||
## 任务信息节点规范
|
||||
- info_nodes 只能包含该任务专属的具体信息节点,严禁关联"用户"、"AI"、"系统"等全局通用实体
|
||||
- 全局实体的信息直接用独立关系记录,不需要通过 Task 中转
|
||||
|
||||
## 可用工具
|
||||
- memory_recall(queryIntent, seedEntities?, depth?, timeRange?, sessionFilter?): 检索记忆
|
||||
@ -252,9 +259,10 @@ ${personaContext || '你是一个帮助用户记录和回忆信息的助手。
|
||||
- task_query(limit?, stateFilter?): 查询任务列表
|
||||
|
||||
## 工具调用规则
|
||||
1. 每轮对话必须按顺序执行:步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求
|
||||
2. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!
|
||||
3. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文
|
||||
1. ⚠️ 在完成所有工具调用之前,绝对不要输出任何文字。先默默调用工具,等所有结果返回后再输出一次完整的回复。
|
||||
2. 每轮对话必须按顺序执行:步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求
|
||||
3. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!
|
||||
4. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文
|
||||
|
||||
## 写入规则
|
||||
用户明确表达以下信息时必须写入记忆:
|
||||
@ -278,10 +286,13 @@ const tripletPropsDict: ToolPropertiesDefinition = {
|
||||
confidence: makeNumberProp('置信度')
|
||||
};
|
||||
const purgeCriteriaDict: ToolPropertiesDefinition = {
|
||||
subjectContains: makeStringProp(''),
|
||||
relationType: makeStringProp(''),
|
||||
targetContains: makeStringProp(''),
|
||||
sessionId: makeStringProp('')
|
||||
subjectContains: makeStringProp('源实体名包含(模糊匹配)'),
|
||||
relationType: makeStringProp('关系类型'),
|
||||
targetContains: makeStringProp('目标实体名包含(模糊匹配)'),
|
||||
sessionId: makeStringProp('会话ID过滤'),
|
||||
sourceType: makeStringProp('源实体类型过滤(如 TaskNode)'),
|
||||
targetType: makeStringProp('目标实体类型过滤'),
|
||||
sourceHasStatus: makeStringProp('源实体状态过滤(如 archived)')
|
||||
};
|
||||
const newRelDict: ToolPropertiesDefinition = {
|
||||
relation: makeStringProp(''),
|
||||
@ -360,23 +371,23 @@ const personaRemoveProps: ToolPropertiesDefinition = {
|
||||
};
|
||||
|
||||
const TOOLS_DEFINITION: ToolFunctionDef[] = [
|
||||
makeToolDef('memory_recall', '检索记忆。支持关键词、种子实体、深度扩展。返回相关实体和关系。', recallProps, ['queryIntent']),
|
||||
makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库。', commitProps, ['triplets']),
|
||||
makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。', purgeProps, ['criteria', 'mode']),
|
||||
makeToolDef('memory_recall', '检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。\n\n【⚠️ 强制执行顺序 - 每轮必须严格遵守】\n1. 步骤1(必须首先执行): 查询人设图\n2. 步骤2(必须第二步执行): 查询工作记忆链\n【重要】跳过步骤1或步骤2将导致系统错误!', recallProps, ['queryIntent']),
|
||||
makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库,支持批量写入。\n\n【重要】写入原则:\n- 用户明确表达的信息 → 必须写入\n- AI推理得到的信息 → 可以写入,但需标注[推测]\n- 避免写入冗余或无意义的信息', commitProps, ['triplets']),
|
||||
makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。\n\n【使用场景】\n- 纠错替代修正错误信息\n- 删除特定类型的节点关系\n- 删除残留在已归档任务上的状态关系\n\n【重要】\n- 优先使用 supersede 模式修正错误\n- 软删除不会物理删除数据', purgeProps, ['criteria', 'mode']),
|
||||
makeToolDef('memory_introspect', '查看记忆状态。返回实体数量、关系数量、热点实体。', introspectProps),
|
||||
makeToolDef('memory_archive', '归档旧记忆。将N天前的非活跃关系标记为归档状态。', archiveProps2, ['days']),
|
||||
makeToolDef('memory_cleanup', '清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。', cleanupProps),
|
||||
makeToolDef('memory_query_archived', '查询已归档的记忆。返回所有 status=archived 的关系记录。', queryArchivedProps),
|
||||
makeToolDef('context_rewrite', '压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。', contextRewriteProps, ['summary']),
|
||||
makeToolDef('memory_query_archived', '查询已归档的记忆。\n\n【使用场景】\n- 想了解之前归档过哪些记忆\n- 按关键词搜索归档内容\n- 按时间范围查看最近归档的历史\n\n【注意】\n- 只返回 status=archived 的原始关系记录\n- days 和 keyword 可以单独使用或组合使用', queryArchivedProps),
|
||||
makeToolDef('context_rewrite', '压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。\n\n【使用场景】\n- 本轮已执行 ≥5 次查询类工具调用\n- 【⚠️ 强制要求】context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!', contextRewriteProps, ['summary']),
|
||||
makeToolDef('persona_update', '更新AI人设属性(语气、风格、性格等)。', personaProps),
|
||||
makeToolDef('persona_remove', '删除单条人设属性。保留其他人设不变。', personaRemoveProps, ['attribute']),
|
||||
makeToolDef('persona_clear', '清除所有人设信息。', EMPTY_PROPS),
|
||||
makeToolDef('task_create', '创建新的工作记忆任务节点。', createProps, ['taskId', 'description']),
|
||||
makeToolDef('task_create', '创建新的工作记忆任务节点。\n\n【重要】info_nodes 只能包含该任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁关联\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', createProps, ['taskId', 'description']),
|
||||
makeToolDef('task_set_state', '设置任务状态。', setStateProps, ['taskId', 'state']),
|
||||
makeToolDef('task_delete', '删除任务节点。', deleteProps, ['taskId']),
|
||||
makeToolDef('task_link_info', '关联信息节点到任务。', linkInfoProps, ['taskId', 'infoNodeNames']),
|
||||
makeToolDef('task_archive', '归档任务。将任务设为已暂停,写入归档摘要。', archiveProps, ['taskId']),
|
||||
makeToolDef('task_query', '查询最近的任务列表。', queryProps)
|
||||
makeToolDef('task_link_info', '关联信息节点到任务。\n\n【重要】info_node_names只能放任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁放\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', linkInfoProps, ['taskId', 'infoNodeNames']),
|
||||
makeToolDef('task_archive', '归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。\n\n【使用场景】\n1. 话题转变时归档旧任务\n2. 已完成的任务及时归档\n3. 长时间无更新的任务归档\n\n【注意】优先使用 task_archive 替代 task_set_state(state=archived),因为它会自动写入完成摘要。', archiveProps, ['taskId']),
|
||||
makeToolDef('task_query', '查询最近的任务列表。按更新时间倒序排列。新对话开始时优先使用此工具获取所有进展中的任务,避免重复创建。', queryProps)
|
||||
];
|
||||
|
||||
// ========= 工具名称映射 =========
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
* 参考:main 分支 core/tools/memory_tools.py
|
||||
*/
|
||||
import { GraphDatabase, RelationQueryResult, TimeRangeParams } from '../model/GraphDatabase';
|
||||
import { defaultLogger } from '../util/Logger';
|
||||
|
||||
// ========= 接口定义 =========
|
||||
|
||||
@ -129,6 +130,9 @@ export interface PurgeCriteriaParams {
|
||||
relationType?: string;
|
||||
targetContains?: string;
|
||||
sessionId?: string;
|
||||
sourceType?: string;
|
||||
targetType?: string;
|
||||
sourceHasStatus?: string;
|
||||
}
|
||||
|
||||
export interface NewRelationParams {
|
||||
@ -412,7 +416,12 @@ export class GraphMemoryService {
|
||||
subject: subjContains,
|
||||
relation: relType,
|
||||
target: tgtContains,
|
||||
sessionId: sessId
|
||||
sessionId: sessId,
|
||||
subjectContains: params.criteria.subjectContains,
|
||||
targetContains: params.criteria.targetContains,
|
||||
sourceType: params.criteria.sourceType,
|
||||
targetType: params.criteria.targetType,
|
||||
sourceHasStatus: params.criteria.sourceHasStatus
|
||||
}, params.mode === 'hard' ? 'hard' : 'soft');
|
||||
const purgeResult: MemoryPurgeResult = {
|
||||
deletedCount: subjContains || tgtContains || relType || sessId ? 1 : 0,
|
||||
@ -619,7 +628,7 @@ export class GraphMemoryService {
|
||||
const result = await this.db.queryArchived(days, keyword);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error('queryArchived error: ' + JSON.stringify(e));
|
||||
defaultLogger.error('queryArchived error: ' + JSON.stringify(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -895,49 +904,9 @@ export class GraphMemoryService {
|
||||
async taskQuery(params?: TaskQueryParams): Promise<TaskQueryResult> {
|
||||
const limit: number = params?.limit ?? 10;
|
||||
const stateFilter: string | undefined = params?.stateFilter;
|
||||
|
||||
// 查询所有 TaskNode 类型的节点
|
||||
const allNodes = await this.db.search('');
|
||||
const taskNodes = allNodes.filter(n => n.type === 'TaskNode');
|
||||
|
||||
const tasks: TaskInfo[] = [];
|
||||
for (const node of taskNodes.slice(0, limit)) {
|
||||
// 查 description 和 has_state
|
||||
const result = await this.db.recall(node.name, [node.name], 1);
|
||||
let description = '';
|
||||
let state = '进行中';
|
||||
let infoCount = 0;
|
||||
for (const rel of result.relations) {
|
||||
if (rel.type === 'description' && rel.source === node.name) {
|
||||
description = rel.target;
|
||||
}
|
||||
if (rel.type === 'has_state' && rel.source === node.name) {
|
||||
state = rel.target;
|
||||
}
|
||||
if (rel.type === 'CONTAINS_INFO' && rel.source === node.name) {
|
||||
infoCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (stateFilter && state !== stateFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const taskInfo: TaskInfo = {
|
||||
taskId: node.name,
|
||||
description,
|
||||
state,
|
||||
infoCount,
|
||||
updatedAt: ''
|
||||
};
|
||||
tasks.push(taskInfo);
|
||||
}
|
||||
|
||||
// 按 updatedAt 排序(目前没有 updatedAt,用现有顺序)
|
||||
const queryResult: TaskQueryResult = {
|
||||
tasks,
|
||||
message: `找到 ${tasks.length} 个任务`
|
||||
tasks: await this.db.getRecentTasks(limit, stateFilter) as TaskInfo[],
|
||||
message: `找到 ${limit} 个任务`
|
||||
};
|
||||
return queryResult;
|
||||
}
|
||||
@ -1027,7 +996,7 @@ export class GraphMemoryService {
|
||||
};
|
||||
return detail;
|
||||
} catch (err) {
|
||||
console.error('getNodeDetail error: ' + JSON.stringify(err));
|
||||
defaultLogger.error('getNodeDetail error: ' + JSON.stringify(err));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
31
common/src/main/ets/util/Logger.ets
Normal file
31
common/src/main/ets/util/Logger.ets
Normal file
@ -0,0 +1,31 @@
|
||||
import { hilog } from "@kit.PerformanceAnalysisKit";
|
||||
|
||||
class Logger {
|
||||
private domain: number;
|
||||
private prefix: string;
|
||||
private format: string = "%{public}s, %{public}s";
|
||||
|
||||
public constructor(prefix: string) {
|
||||
this.prefix = prefix;
|
||||
this.domain = 0xFF00;
|
||||
}
|
||||
|
||||
public debug(...args: Object[]): void {
|
||||
hilog.debug(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
|
||||
public info(...args: Object[]): void {
|
||||
hilog.info(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
|
||||
public warn(...args: Object[]): void {
|
||||
hilog.warn(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
|
||||
public error(...args: Object[]): void {
|
||||
hilog.error(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultLogger = new Logger("[TrulyMEM]");
|
||||
export default defaultLogger;
|
||||
@ -1,13 +1,12 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "common",
|
||||
"type": "shared",
|
||||
"description": "TrulyMEM common shared module",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM common module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"deliveryWithInstall": true
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
# Define project specific obfuscation rules here.
|
||||
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||
#
|
||||
# For more details, see
|
||||
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
|
||||
|
||||
# Obfuscation options:
|
||||
# -disable-obfuscation: disable all obfuscations
|
||||
# -enable-property-obfuscation: obfuscate the property names
|
||||
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||
# -compact: remove unnecessary blank spaces and all line feeds
|
||||
# -remove-log: remove all console.* statements
|
||||
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||
# -apply-namecache: reuse the given cache file
|
||||
|
||||
# Keep options:
|
||||
# -keep-property-name: specifies property names that you want to keep
|
||||
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||
|
||||
-enable-property-obfuscation
|
||||
-enable-toplevel-obfuscation
|
||||
-enable-filename-obfuscation
|
||||
-enable-export-obfuscation
|
||||
|
||||
# 保留JSON解析相关的属性名,防止混淆导致JSON解析失败
|
||||
-keep-property-name
|
||||
id
|
||||
name
|
||||
nameEn
|
||||
icon
|
||||
isCustom
|
||||
builtin
|
||||
custom
|
||||
pixelMap
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "entry",
|
||||
"version": "1.0.0",
|
||||
"description": "TrulyMEM - A personal memory and knowledge graph application.",
|
||||
"main": "",
|
||||
"author": "",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"trulymem-core": "file:../trulymem-core"
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
|
||||
export default class EntryAbility extends UIAbility {
|
||||
onCreate(want: Want, param: AbilityConstant.LaunchParam): void {
|
||||
console.info('EntryAbility onCreate');
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
console.info('EntryAbility onDestroy');
|
||||
}
|
||||
|
||||
onWindowStageCreate(windowStage: window.WindowStage): void {
|
||||
const windowClass: window.Window = windowStage.getMainWindowSync();
|
||||
|
||||
try {
|
||||
windowClass.setWindowBackgroundColor('#00000000');
|
||||
} catch (e) {
|
||||
console.error('Failed to set background color: ' + (e as BusinessError).message);
|
||||
}
|
||||
|
||||
try {
|
||||
windowClass.setWindowSystemBarProperties({
|
||||
statusBarColor: '#00000000',
|
||||
navigationBarColor: '#00000000',
|
||||
statusBarContentColor: '#FFFFFF',
|
||||
navigationBarContentColor: '#FFFFFF'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to set system bar: ' + (e as BusinessError).message);
|
||||
}
|
||||
|
||||
AppStorage.setOrCreate('main_window', windowClass);
|
||||
|
||||
windowStage.loadContent('pages/Index', (err: BusinessError) => {
|
||||
if (err.code) {
|
||||
console.error('Failed to load content: ' + err.message);
|
||||
return;
|
||||
}
|
||||
console.info('Succeeded in loading content');
|
||||
});
|
||||
}
|
||||
|
||||
onWindowStageDestroy(): void {
|
||||
console.info('EntryAbility onWindowStageDestroy');
|
||||
}
|
||||
|
||||
onForeground(): void {
|
||||
console.info('EntryAbility onForeground');
|
||||
}
|
||||
|
||||
onBackground(): void {
|
||||
console.info('EntryAbility onBackground');
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "entry",
|
||||
"type": "har",
|
||||
"description": "$string:module_desc",
|
||||
"deviceTypes": ["phone", "tablet", "2in1"],
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": false,
|
||||
"pages": "$profile:main_pages",
|
||||
"requestPermissions": [
|
||||
{
|
||||
"name": "ohos.permission.INTERNET"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.GET_NETWORK_INFO"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<rect width="200" height="200" rx="30" fill="#6366f1"/>
|
||||
<text x="100" y="130" font-family="Arial" font-size="80" fill="white" text-anchor="middle" font-weight="bold">T</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 313 B |
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<rect width="200" height="200" rx="20" fill="#6366f1"/>
|
||||
<text x="100" y="130" font-family="Arial" font-size="60" fill="white" text-anchor="middle" font-weight="bold">TM</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 314 B |
@ -1,5 +0,0 @@
|
||||
{
|
||||
"src": [
|
||||
"pages/Index"
|
||||
]
|
||||
}
|
||||
17
features/chat/BuildProfile.ets
Normal file
17
features/chat/BuildProfile.ets
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||
*/
|
||||
export const HAR_VERSION = '1.0.0';
|
||||
export const BUILD_MODE_NAME = 'debug';
|
||||
export const DEBUG = true;
|
||||
export const TARGET_NAME = 'default';
|
||||
|
||||
/**
|
||||
* BuildProfile Class is used only for compatibility purposes.
|
||||
*/
|
||||
export default class BuildProfile {
|
||||
static readonly HAR_VERSION = HAR_VERSION;
|
||||
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||
static readonly DEBUG = DEBUG;
|
||||
static readonly TARGET_NAME = TARGET_NAME;
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
|
||||
1
features/chat/chat
Symbolic link
1
features/chat/chat
Symbolic link
@ -0,0 +1 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/features/chat
|
||||
6
features/chat/hvigorfile.ts
Normal file
6
features/chat/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -6,13 +6,13 @@
|
||||
"lockfileVersion": 3,
|
||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||
"specifiers": {
|
||||
"librcon@../librcon": "librcon@../librcon"
|
||||
"@ohos/common@../../common": "@ohos/common@../../common"
|
||||
},
|
||||
"packages": {
|
||||
"librcon@../librcon": {
|
||||
"name": "librcon",
|
||||
"@ohos/common@../../common": {
|
||||
"name": "@ohos/common",
|
||||
"version": "1.0.0",
|
||||
"resolved": "../librcon",
|
||||
"resolved": "../../common",
|
||||
"registryType": "local"
|
||||
}
|
||||
}
|
||||
1
features/chat/oh_modules/@ohos/common
Symbolic link
1
features/chat/oh_modules/@ohos/common
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../common
|
||||
161
features/chat/src/main/ets/components/ChatComponents.ets
Normal file
161
features/chat/src/main/ets/components/ChatComponents.ets
Normal file
@ -0,0 +1,161 @@
|
||||
import { ChatMessage } from '@ohos/common';
|
||||
|
||||
/**
|
||||
* ChatMessageBubble — 单条聊天消息气泡
|
||||
* 封装消息的角色标识、内容样式、玻璃拟态背景
|
||||
*/
|
||||
@Component
|
||||
export struct ChatMessageBubble {
|
||||
@ObjectLink msg: ChatMessage;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 角色标识
|
||||
Text(this.msg.role === 'user' ? '🧑 你' : '🤖 AI')
|
||||
.fontSize(11)
|
||||
.fontColor(this.msg.role === 'user' ? '#7C4DFF' : '#999')
|
||||
.width('100%')
|
||||
|
||||
// 消息内容
|
||||
Text(this.msg.content)
|
||||
.fontSize(15)
|
||||
.width('100%')
|
||||
.margin({ top: 4 })
|
||||
.fontColor('#FFFFFF')
|
||||
}
|
||||
.padding(12)
|
||||
.backgroundColor(this.msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)')
|
||||
.borderRadius(12)
|
||||
.border({
|
||||
width: 1,
|
||||
color: this.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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ThinkingIndicator — AI 思考中指示器
|
||||
* 玻璃拟态加载动画 + 文字提示
|
||||
*/
|
||||
@Component
|
||||
export struct ThinkingIndicator {
|
||||
build() {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ToolCallLogPanel — 工具调用日志面板
|
||||
* 橙色风格,显示 Agent 调用的工具链
|
||||
*/
|
||||
@Component
|
||||
export struct ToolCallLogPanel {
|
||||
@Prop logText: string;
|
||||
|
||||
build() {
|
||||
Text(this.logText)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatInputBar — 底部输入栏
|
||||
* TextArea + 发送按钮,主题色边框
|
||||
*/
|
||||
@Component
|
||||
export struct ChatInputBar {
|
||||
@Link inputText: string;
|
||||
@Prop isThinking: boolean;
|
||||
onSend?: () => void;
|
||||
|
||||
build() {
|
||||
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.onSend?.(); })
|
||||
.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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatMessageList — 聊天消息列表容器
|
||||
* 整合消息气泡、思考指示器、工具日志
|
||||
*/
|
||||
@Component
|
||||
export struct ChatMessageList {
|
||||
@Prop messages: ChatMessage[];
|
||||
@Prop isThinking: boolean;
|
||||
@Prop toolCallLog: string;
|
||||
private scrollController: Scroller = new Scroller();
|
||||
|
||||
build() {
|
||||
List() {
|
||||
ForEach(this.messages, (msg: ChatMessage) => {
|
||||
ListItem() {
|
||||
ChatMessageBubble({ msg: msg })
|
||||
}
|
||||
})
|
||||
|
||||
if (this.isThinking) {
|
||||
ListItem() {
|
||||
ThinkingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.toolCallLog && !this.isThinking) {
|
||||
ListItem() {
|
||||
ToolCallLogPanel({ logText: this.toolCallLog })
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.backgroundColor('rgba(0,0,0,0.1)')
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import { GraphDatabase } from '@ohos/common';
|
||||
import { GraphMemoryService } from '@ohos/common';
|
||||
import { AIAgentService, ChatMessage, AgentResponse } from '@ohos/common';
|
||||
import { GraphDatabase, GraphMemoryService, AIAgentService, ChatMessage, AgentResponse, Logger } from '@ohos/common';
|
||||
import { ChatMessageList, ChatInputBar } from '../components/ChatComponents';
|
||||
|
||||
@Component
|
||||
export struct ChatPage {
|
||||
@ -9,7 +8,6 @@ export struct ChatPage {
|
||||
@Prop db: GraphDatabase;
|
||||
@State toolCallLog: string = '';
|
||||
@State isThinking: boolean = false;
|
||||
private scrollController: Scroller = new Scroller();
|
||||
private agentService?: AIAgentService;
|
||||
|
||||
async aboutToAppear() {
|
||||
@ -62,7 +60,7 @@ export struct ChatPage {
|
||||
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));
|
||||
Logger.error('Agent request failed: ' + JSON.stringify(err));
|
||||
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${err.message || JSON.stringify(err)}` }];
|
||||
} finally {
|
||||
this.isThinking = false;
|
||||
@ -71,101 +69,20 @@ export struct ChatPage {
|
||||
|
||||
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%')
|
||||
ChatMessageList({
|
||||
messages: this.messages,
|
||||
isThinking: this.isThinking,
|
||||
toolCallLog: this.toolCallLog
|
||||
})
|
||||
|
||||
// 消息内容
|
||||
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 })
|
||||
ChatInputBar({
|
||||
inputText: this.inputText,
|
||||
isThinking: this.isThinking,
|
||||
onSend: (): void => { this.sendMessage(); }
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor('rgba(26,27,46,0.95)')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "librcon",
|
||||
"name": "chat",
|
||||
"type": "har",
|
||||
"requestPermissions": [
|
||||
],
|
||||
"description": "TrulyMEM chat feature module",
|
||||
"deviceTypes": [
|
||||
"default",
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
@ -1 +0,0 @@
|
||||
// Common business module - placeholder for shared business logic
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "@ohos/commonbusiness",
|
||||
"version": "1.0.0",
|
||||
"description": "TrulyMEM common business module",
|
||||
"main": "Index.ets",
|
||||
"author": "",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"@ohos/common": "file:../../common"
|
||||
}
|
||||
}
|
||||
17
features/graph/BuildProfile.ets
Normal file
17
features/graph/BuildProfile.ets
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||
*/
|
||||
export const HAR_VERSION = '1.0.0';
|
||||
export const BUILD_MODE_NAME = 'debug';
|
||||
export const DEBUG = true;
|
||||
export const TARGET_NAME = 'default';
|
||||
|
||||
/**
|
||||
* BuildProfile Class is used only for compatibility purposes.
|
||||
*/
|
||||
export default class BuildProfile {
|
||||
static readonly HAR_VERSION = HAR_VERSION;
|
||||
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||
static readonly DEBUG = DEBUG;
|
||||
static readonly TARGET_NAME = TARGET_NAME;
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
|
||||
1
features/graph/graph
Symbolic link
1
features/graph/graph
Symbolic link
@ -0,0 +1 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/features/graph
|
||||
6
features/graph/hvigorfile.ts
Normal file
6
features/graph/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
19
features/graph/oh-package-lock.json5
Normal file
19
features/graph/oh-package-lock.json5
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"meta": {
|
||||
"stableOrder": true,
|
||||
"enableUnifiedLockfile": false
|
||||
},
|
||||
"lockfileVersion": 3,
|
||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||
"specifiers": {
|
||||
"@ohos/common@../../common": "@ohos/common@../../common"
|
||||
},
|
||||
"packages": {
|
||||
"@ohos/common@../../common": {
|
||||
"name": "@ohos/common",
|
||||
"version": "1.0.0",
|
||||
"resolved": "../../common",
|
||||
"registryType": "local"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
features/graph/oh_modules/@ohos/common
Symbolic link
1
features/graph/oh_modules/@ohos/common
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../common
|
||||
251
features/graph/src/main/ets/components/GraphComponents.ets
Normal file
251
features/graph/src/main/ets/components/GraphComponents.ets
Normal file
@ -0,0 +1,251 @@
|
||||
import web_webview from '@ohos.web.webview';
|
||||
import { GraphDatabase, RecallEntity, GraphMemoryService, ConnectionItem, NodeDetailInfo, Logger } from '@ohos/common';
|
||||
|
||||
/**
|
||||
* GraphNodeSearchBar — 图节点搜索栏
|
||||
* 悬浮在 WebView 上方的搜索输入框
|
||||
*/
|
||||
@Component
|
||||
export struct GraphNodeSearchBar {
|
||||
@Link searchText: string;
|
||||
onSearchInput?: (value: string) => void;
|
||||
|
||||
build() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NodeDetailPanel — 节点详情浮层
|
||||
* 显示选中节点的名称、类型、提及次数、连接关系
|
||||
*/
|
||||
@Component
|
||||
export struct NodeDetailPanel {
|
||||
@Prop detail: NodeDetailInfo;
|
||||
onClose?: () => void;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Column() {
|
||||
Text(this.detail.name)
|
||||
.fontSize(18)
|
||||
.fontColor('#44ff88')
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.margin({ bottom: 10 })
|
||||
|
||||
Text('类型: ' + this.detail.type)
|
||||
.fontSize(14)
|
||||
.fontColor('#aaaacc')
|
||||
|
||||
Text('提及次数: ' + this.detail.mention_count)
|
||||
.fontSize(14)
|
||||
.fontColor('#aaaacc')
|
||||
|
||||
Text('连接数: ' + this.detail.connection_count)
|
||||
.fontSize(14)
|
||||
.fontColor('#aaaacc')
|
||||
|
||||
if (this.detail.connections && this.detail.connections.length > 0) {
|
||||
Text('连接关系:')
|
||||
.fontSize(14)
|
||||
.fontColor('#8888aa')
|
||||
.margin({ top: 10, bottom: 5 })
|
||||
List() {
|
||||
ForEach(this.detail.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.onClose?.();
|
||||
})
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphWebView — 图可视化 WebView 封装
|
||||
* 包含 WebView 配置、JS Bridge 注册、数据加载回调
|
||||
*/
|
||||
@Component
|
||||
export struct GraphWebView {
|
||||
private controller: web_webview.WebviewController = new web_webview.WebviewController();
|
||||
private bridge?: NativeBridge;
|
||||
|
||||
onPageEnd?: () => void;
|
||||
|
||||
getController(): web_webview.WebviewController {
|
||||
return this.controller;
|
||||
}
|
||||
|
||||
setBridge(bridge: NativeBridge): void {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
build() {
|
||||
Web({ src: $rawfile('graph.html'), controller: this.controller })
|
||||
.javaScriptAccess(true)
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.zoomAccess(true)
|
||||
.onPageEnd(() => {
|
||||
this.onPageEnd?.();
|
||||
})
|
||||
.javaScriptProxy({
|
||||
object: this.bridge,
|
||||
name: 'nativeBridge',
|
||||
methodList: ['onNodeClick', 'onSearch'],
|
||||
asyncMethodList: ['requestGraphData'],
|
||||
controller: this.controller
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NativeBridge — WebView 原生桥接类(移动自 GraphPage)
|
||||
* 负责 ArkTS ↔ WebView JavaScript 双向通信
|
||||
*/
|
||||
export 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 {
|
||||
Logger.info('Node clicked: id=' + nodeId + ', name=' + nodeName);
|
||||
if (this.onNodeClickCallback) {
|
||||
this.onNodeClickCallback(nodeId, nodeName);
|
||||
}
|
||||
}
|
||||
|
||||
onSearch(query: string): void {
|
||||
Logger.info('Search from WebView: ' + query);
|
||||
if (this.onSearchCallback) {
|
||||
this.onSearchCallback(query);
|
||||
}
|
||||
}
|
||||
|
||||
requestGraphData(): void {
|
||||
Logger.info('requestGraphData called from WebView');
|
||||
if (this.onRequestGraphData) {
|
||||
this.onRequestGraphData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphDataService — 图数据查询服务
|
||||
* 封装从 GraphDatabase 读取节点和边的逻辑
|
||||
*/
|
||||
export class GraphDataService {
|
||||
private db: GraphDatabase;
|
||||
|
||||
constructor(db: GraphDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getAllNodes(): Promise<GraphNodeItem[]> {
|
||||
const result = await this.db.search('');
|
||||
return result.map((r, idx): GraphNodeItem => {
|
||||
return {
|
||||
id: idx + 1,
|
||||
label: r.name,
|
||||
type: r.type,
|
||||
mentions: r.mentions
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getAllEdges(): 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 = nameToId[r.source];
|
||||
const targetId = nameToId[r.target];
|
||||
if (sourceId !== undefined && targetId !== undefined) {
|
||||
edgeItems.push({
|
||||
id: i + 1,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
label: r.type,
|
||||
relation: r.type
|
||||
});
|
||||
}
|
||||
}
|
||||
return edgeItems;
|
||||
}
|
||||
}
|
||||
|
||||
// ========= 内部类型定义 =========
|
||||
|
||||
interface GraphNodeItem {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
|
||||
interface GraphEdgeItem {
|
||||
id: number;
|
||||
source: number;
|
||||
target: number;
|
||||
label: string;
|
||||
relation: string;
|
||||
}
|
||||
@ -1,13 +1,25 @@
|
||||
/**
|
||||
* GraphPage — 记忆星图页面
|
||||
* GraphPage — 记忆星图页面(重构后)
|
||||
* 使用 WebView 显示 Three.js 3D 图可视化
|
||||
* 通过 javaScriptProxy 与 WebView 双向通信
|
||||
* 子组件:GraphWebView、GraphNodeSearchBar、NodeDetailPanel、GraphDataService、NativeBridge
|
||||
*/
|
||||
import web_webview from '@ohos.web.webview';
|
||||
import { GraphDatabase, RecallEntity } from '@ohos/common';
|
||||
import { GraphMemoryService, ConnectionItem, NodeDetailInfo } from '@ohos/common';
|
||||
import {
|
||||
GraphDatabase,
|
||||
NodeDetailInfo,
|
||||
Logger,
|
||||
RecallEntity,
|
||||
GraphMemoryService
|
||||
} from '@ohos/common';
|
||||
import {
|
||||
GraphWebView,
|
||||
GraphNodeSearchBar,
|
||||
NodeDetailPanel,
|
||||
GraphDataService,
|
||||
NativeBridge
|
||||
} from '../components/GraphComponents';
|
||||
|
||||
// ========= 图数据结构定义 =========
|
||||
// ========= GraphPage 组件 =========
|
||||
|
||||
interface GraphNodeItem {
|
||||
id: number;
|
||||
@ -24,52 +36,6 @@ interface GraphEdgeItem {
|
||||
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();
|
||||
@ -101,14 +67,13 @@ export struct GraphPage {
|
||||
*/
|
||||
private async handleNodeClick(nodeId: number, nodeName: string): Promise<void> {
|
||||
try {
|
||||
// 从数据库查询节点详细信息
|
||||
const detail = await this.graphService.getNodeDetail(nodeName);
|
||||
const detail: NodeDetailInfo | null = await this.graphService.getNodeDetail(nodeName);
|
||||
if (detail) {
|
||||
this.selectedNodeDetail = detail;
|
||||
this.showNodeDetail = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('handleNodeClick error: ' + JSON.stringify(err));
|
||||
Logger.error('handleNodeClick error: ' + JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,49 +102,20 @@ export struct GraphPage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库读取全量图数据,通过 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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库查询所有节点
|
||||
* 从数据库读取全量图数据,推送给 WebView
|
||||
*/
|
||||
private async getAllNodesData(): Promise<GraphNodeItem[]> {
|
||||
const result = await this.db.search('');
|
||||
const items: GraphNodeItem[] = result.map((r, idx): GraphNodeItem => {
|
||||
const item: GraphNodeItem = {
|
||||
return result.map((r, idx): GraphNodeItem => {
|
||||
return {
|
||||
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> = {};
|
||||
@ -192,19 +128,39 @@ export struct GraphPage {
|
||||
const sourceId: number | undefined = nameToId[r.source];
|
||||
const targetId: number | undefined = nameToId[r.target];
|
||||
if (sourceId !== undefined && targetId !== undefined) {
|
||||
const edgeItem: GraphEdgeItem = {
|
||||
edgeItems.push({
|
||||
id: i + 1,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
label: r.type,
|
||||
relation: r.type
|
||||
};
|
||||
edgeItems.push(edgeItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
return edgeItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库读取全量图数据,推送给 WebView
|
||||
*/
|
||||
private async pushGraphDataToWebView(): Promise<void> {
|
||||
try {
|
||||
const allNodes: GraphNodeItem[] = await this.getAllNodesData();
|
||||
const allEdges: GraphEdgeItem[] = await this.getAllEdgesData();
|
||||
|
||||
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) {
|
||||
Logger.error('pushGraphDataToWebView error: ' + JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Stack() {
|
||||
// WebView 显示 3D 星图
|
||||
@ -212,11 +168,10 @@ export struct GraphPage {
|
||||
.javaScriptAccess(true)
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.zoomAccess(true) // 启用缩放(自带双指捏合)
|
||||
.zoomAccess(true)
|
||||
.onPageEnd(() => {
|
||||
this.pushGraphDataToWebView();
|
||||
})
|
||||
// 注册原生桥接对象,供 WebView JavaScript 调用
|
||||
.javaScriptProxy({
|
||||
object: this.bridge,
|
||||
name: 'nativeBridge',
|
||||
@ -225,89 +180,21 @@ export struct GraphPage {
|
||||
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)
|
||||
// 搜索框
|
||||
GraphNodeSearchBar({
|
||||
searchText: this.searchText,
|
||||
onSearchInput: (value: string): void => { this.onSearchInput(value); }
|
||||
})
|
||||
|
||||
// 节点详情浮层
|
||||
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)
|
||||
NodeDetailPanel({
|
||||
detail: this.selectedNodeDetail,
|
||||
onClose: (): void => { this.closeNodeDetail(); }
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
}
|
||||
12
features/graph/src/main/module.json5
Normal file
12
features/graph/src/main/module.json5
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "graph",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM graph feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
17
features/settings/BuildProfile.ets
Normal file
17
features/settings/BuildProfile.ets
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||
*/
|
||||
export const HAR_VERSION = '1.0.0';
|
||||
export const BUILD_MODE_NAME = 'debug';
|
||||
export const DEBUG = true;
|
||||
export const TARGET_NAME = 'default';
|
||||
|
||||
/**
|
||||
* BuildProfile Class is used only for compatibility purposes.
|
||||
*/
|
||||
export default class BuildProfile {
|
||||
static readonly HAR_VERSION = HAR_VERSION;
|
||||
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||
static readonly DEBUG = DEBUG;
|
||||
static readonly TARGET_NAME = TARGET_NAME;
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
|
||||
6
features/settings/hvigorfile.ts
Normal file
6
features/settings/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
19
features/settings/oh-package-lock.json5
Normal file
19
features/settings/oh-package-lock.json5
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"meta": {
|
||||
"stableOrder": true,
|
||||
"enableUnifiedLockfile": false
|
||||
},
|
||||
"lockfileVersion": 3,
|
||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||
"specifiers": {
|
||||
"@ohos/common@../../common": "@ohos/common@../../common"
|
||||
},
|
||||
"packages": {
|
||||
"@ohos/common@../../common": {
|
||||
"name": "@ohos/common",
|
||||
"version": "1.0.0",
|
||||
"resolved": "../../common",
|
||||
"registryType": "local"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
features/settings/oh_modules/@ohos/common
Symbolic link
1
features/settings/oh_modules/@ohos/common
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../common
|
||||
1
features/settings/settings
Symbolic link
1
features/settings/settings
Symbolic link
@ -0,0 +1 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/features/settings
|
||||
105
features/settings/src/main/ets/components/SettingsComponents.ets
Normal file
105
features/settings/src/main/ets/components/SettingsComponents.ets
Normal file
@ -0,0 +1,105 @@
|
||||
import dataPreferences from '@ohos.data.preferences';
|
||||
|
||||
/**
|
||||
* SettingsSectionHeader — 设置区块标题
|
||||
* 大标题 + 加粗白色
|
||||
*/
|
||||
@Component
|
||||
export struct SettingsSectionHeader {
|
||||
@Prop title: string;
|
||||
|
||||
build() {
|
||||
Text(this.title)
|
||||
.fontSize(24)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#FFFFFF')
|
||||
.margin({ top: 20, bottom: 16 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SettingInputItem — 设置输入项
|
||||
* 标签 + TextInput,统一玻璃拟态风格
|
||||
*/
|
||||
@Component
|
||||
export struct SettingInputItem {
|
||||
@Prop label: string;
|
||||
@Prop placeholder: string;
|
||||
@Link value: string;
|
||||
isPassword?: boolean = false;
|
||||
onValueChange?: (value: string) => void;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Text(this.label)
|
||||
.fontSize(14)
|
||||
.fontColor('#FFFFFF')
|
||||
.width('100%')
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: this.placeholder, text: this.value })
|
||||
.type(this.isPassword ? InputType.Password : InputType.Normal)
|
||||
.onChange((v: string) => {
|
||||
this.value = v;
|
||||
this.onValueChange?.(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 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GlowBackground — 主题色光晕背景装饰
|
||||
* 用于设置页顶部装饰
|
||||
*/
|
||||
@Component
|
||||
export struct GlowBackground {
|
||||
@Prop color: string = 'rgba(124,77,255,0.15)';
|
||||
@Prop glowSize: number = 200;
|
||||
|
||||
build() {
|
||||
Column()
|
||||
.width(this.glowSize)
|
||||
.height(this.glowSize)
|
||||
.backgroundColor(this.color)
|
||||
.blur(40)
|
||||
.borderRadius(this.glowSize / 2)
|
||||
.position({ x: '10%', y: '20%' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AppConfigStore — 应用配置存储封装
|
||||
* 封装 Preferences 读写,提供类型安全访问
|
||||
*/
|
||||
export class AppConfigStore {
|
||||
private pref?: dataPreferences.Preferences;
|
||||
private readonly storeName: string = 'trulymem_config';
|
||||
|
||||
async init(ctx: Context): Promise<void> {
|
||||
this.pref = await dataPreferences.getPreferences(ctx, this.storeName);
|
||||
}
|
||||
|
||||
async getString(key: string, defaultValue: string): Promise<string> {
|
||||
return String(await this.pref?.get(key, defaultValue));
|
||||
}
|
||||
|
||||
async setString(key: string, value: string): Promise<void> {
|
||||
await this.pref?.put(key, value);
|
||||
await this.pref?.flush();
|
||||
}
|
||||
|
||||
static async create(ctx: Context): Promise<AppConfigStore> {
|
||||
const store = new AppConfigStore();
|
||||
await store.init(ctx);
|
||||
return store;
|
||||
}
|
||||
}
|
||||
@ -1,119 +1,53 @@
|
||||
import dataPreferences from '@ohos.data.preferences';
|
||||
import { SettingsSectionHeader, SettingInputItem, GlowBackground, AppConfigStore } from '../components/SettingsComponents';
|
||||
|
||||
@Component
|
||||
export struct SettingsPage {
|
||||
@State baseUrl: string = '';
|
||||
@State model: string = '';
|
||||
@State apiKey: string = '';
|
||||
private pref?: dataPreferences.Preferences;
|
||||
private store: AppConfigStore = new AppConfigStore();
|
||||
|
||||
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', ''));
|
||||
await this.store.init(ctx);
|
||||
this.baseUrl = await this.store.getString('base_url', 'https://api.deepseek.com');
|
||||
this.model = await this.store.getString('model', 'deepseek-chat');
|
||||
this.apiKey = await this.store.getString('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();
|
||||
private async saveConfig(key: string, value: string): Promise<void> {
|
||||
await this.store.setString(key, value);
|
||||
}
|
||||
|
||||
build() {
|
||||
Stack() {
|
||||
// 主题色光晕背景
|
||||
Column()
|
||||
.width(200)
|
||||
.height(200)
|
||||
.backgroundColor('rgba(124,77,255,0.15)')
|
||||
.blur(40)
|
||||
.borderRadius(100)
|
||||
.position({ x: '10%', y: '20%' })
|
||||
GlowBackground()
|
||||
|
||||
Column() {
|
||||
Text('API 配置')
|
||||
.fontSize(24)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor('#FFFFFF')
|
||||
.margin({ top: 20, bottom: 16 })
|
||||
SettingsSectionHeader({ title: 'API 配置' })
|
||||
|
||||
// Base URL 设置项
|
||||
Column() {
|
||||
Text('Base URL')
|
||||
.fontSize(14)
|
||||
.fontColor('#FFFFFF')
|
||||
.width('100%')
|
||||
.margin({ bottom: 8 })
|
||||
SettingInputItem({
|
||||
label: 'Base URL',
|
||||
placeholder: 'https://api.deepseek.com',
|
||||
value: this.baseUrl,
|
||||
onValueChange: (v: string): void => { this.saveConfig('base_url', v); }
|
||||
})
|
||||
|
||||
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 })
|
||||
SettingInputItem({
|
||||
label: 'Model ID',
|
||||
placeholder: 'deepseek-chat',
|
||||
value: this.model,
|
||||
onValueChange: (v: string): void => { this.saveConfig('model', v); }
|
||||
})
|
||||
|
||||
// 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 })
|
||||
SettingInputItem({
|
||||
label: 'API Key',
|
||||
placeholder: 'sk-...',
|
||||
value: this.apiKey,
|
||||
isPassword: true,
|
||||
onValueChange: (v: string): void => { this.saveConfig('api_key', v); }
|
||||
})
|
||||
}
|
||||
.padding(16)
|
||||
.width('100%')
|
||||
|
||||
12
features/settings/src/main/module.json5
Normal file
12
features/settings/src/main/module.json5
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "settings",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM settings feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
25
generate-debug-cert.sh
Normal file
25
generate-debug-cert.sh
Normal file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
DIR="/home/program/.harmonyos"
|
||||
mkdir -p "$DIR"
|
||||
cd "$DIR"
|
||||
|
||||
KEYSTORE_PASS="123456"
|
||||
ALIAS="debug"
|
||||
ALIAS_PASS="123456"
|
||||
DNAME="CN=Debug,OU=Debug,O=TrulyMEM,L=Beijing,ST=Beijing,C=CN"
|
||||
|
||||
# 生成私钥
|
||||
openssl ecparam -genkey -name prime256v1 -out private.pem 2>/dev/null
|
||||
|
||||
# 生成 CSR
|
||||
openssl req -new -key private.pem -out cert.csr -subj "$DNAME" 2>/dev/null
|
||||
|
||||
# 自签名证书
|
||||
openssl req -x509 -days 3650 -key private.pem -in cert.csr -out debug.cer 2>/dev/null
|
||||
|
||||
# 创建 PKCS12
|
||||
openssl pkcs12 -export -out debug.p12 -inkey private.pem -in debug.cer -password pass:$KEYSTORE_PASS -name $ALIAS 2>/dev/null
|
||||
|
||||
echo "Debug cert generated at $DIR"
|
||||
ls -la "$DIR"
|
||||
28
oh-package-lock.json5
Normal file
28
oh-package-lock.json5
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"meta": {
|
||||
"stableOrder": true,
|
||||
"enableUnifiedLockfile": false
|
||||
},
|
||||
"lockfileVersion": 3,
|
||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||
"specifiers": {
|
||||
"@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0",
|
||||
"@ohos/hypium@1.0.24": "@ohos/hypium@1.0.24"
|
||||
},
|
||||
"packages": {
|
||||
"@ohos/hamock@1.0.0": {
|
||||
"name": "@ohos/hamock",
|
||||
"version": "1.0.0",
|
||||
"integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==",
|
||||
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har",
|
||||
"registryType": "ohpm"
|
||||
},
|
||||
"@ohos/hypium@1.0.24": {
|
||||
"name": "@ohos/hypium",
|
||||
"version": "1.0.24",
|
||||
"integrity": "sha512-3dCqc+BAR5LqEGG2Vtzi8O3r7ci/3fYU+FWjwvUobbfko7DUnXGOccaror0yYuUhJfXzFK0aZNMGSnXaTwEnbw==",
|
||||
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.24.har",
|
||||
"registryType": "ohpm"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,7 @@
|
||||
{
|
||||
"modelVersion": "5.0.5",
|
||||
"description": "Please describe the basic information.",
|
||||
"dependencies": {
|
||||
"@kit.UIDesignKit": "^6.1.0",
|
||||
"@kit.ArkUI": "^6.1.0",
|
||||
"@kit.AbilityKit": "^6.1.0",
|
||||
"@kit.BasicServicesKit": "^6.1.0"
|
||||
},
|
||||
"description": "TrulyMEM - True Human Memory",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@ohos/hypium": "1.0.24",
|
||||
"@ohos/hamock": "1.0.0"
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
## 1.0.0
|
||||
- 修复once断言问题
|
||||
## 1.0.0-rc
|
||||
- 提供DevEco Studio预览器场景使能的MockSetup装饰器
|
||||
@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@ -0,0 +1,82 @@
|
||||
# Hamock
|
||||
|
||||
## 简介
|
||||
|
||||
Hamock 是 OpenHarmony 上的模拟框架,提供预览场景的模拟功能。
|
||||
|
||||
## 下载安装
|
||||
|
||||
```bash
|
||||
ohpm install @ohos/hamock
|
||||
```
|
||||
|
||||
OpenHarmony ohpm 环境配置等更多内容,请参考[如何安装 OpenHarmony ohpm 包](https://gitee.com/openharmony-tpc/docs/blob/master/OpenHarmony_har_usage.md)
|
||||
|
||||
## 使用示例
|
||||
|
||||
Hamock 提供了 @MockSetup 用于修饰 Mock 方法,仅支持声明式范式的组件。当开发者预览该组件时,预览运行时将在组件初始化时执行被 @MockSetup 修饰的方法。因此,开发者可以在这个被修饰的方法内重定义组件的方法或重赋值组件的属性,其将在预览时生效。
|
||||
|
||||
> 说明:
|
||||
> @MockSetup 修饰的方法仅在预览场景会自动触发,并先于组件的 aboutToAppear 执行。
|
||||
|
||||
### UI组件的方法
|
||||
|
||||
在 ArkTS 页面代码中引入 Hamock。在目标组件中定义一个方法,并用 @MockSetup 修饰该方法。在这个方法中,使用 MockKit 模拟目标方法。
|
||||
|
||||
```typescript
|
||||
import { MockKit, when, MockSetup } from '@ohos/hamock';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct Index {
|
||||
...
|
||||
@MockSetup
|
||||
randomName() {
|
||||
let mocker: MockKit = new MockKit();
|
||||
let mockfunc: Object = mocker.mockFunc(this, this.method1);
|
||||
// mock 指定的方法在指定入参的返回值
|
||||
when(mockfunc)('test').afterReturn(1);
|
||||
}
|
||||
...
|
||||
// 业务场景调用方法
|
||||
const result: number = this.method1('test'); // in previewer, result = 1
|
||||
}
|
||||
```
|
||||
|
||||
### UI组件的属性
|
||||
|
||||
在 ArkTS 页面代码中引入 Hamock。在目标组件中定义一个方法,并用 @MockSetup 修饰该方法。在这个方法中,对于需要 Mock 的属性,可以重新赋值。
|
||||
|
||||
```typescript
|
||||
import { MockSetup } from '@ohos/hamock';
|
||||
|
||||
@Component
|
||||
struct Person {
|
||||
@Prop species: string;
|
||||
...
|
||||
// 在 @MockSetup 片段中,定义对象属性
|
||||
@MockSetup
|
||||
randomName() {
|
||||
this.species = 'primates';
|
||||
}
|
||||
...
|
||||
// 业务场景调用属性(如果从初始化到调用期间,该属性无变化)
|
||||
const result: string = this.species; // in previewer, result = primates
|
||||
}
|
||||
```
|
||||
|
||||
## 约束与限制
|
||||
|
||||
在下述版本验证通过:
|
||||
|
||||
DevEco Studio: 4.1 (4.1.3.400), SDK: API11 (4.1.0.36)
|
||||
|
||||
MockSetup 仅在 API11 支持。
|
||||
|
||||
## 贡献代码
|
||||
|
||||
使用过程中发现任何问题都可以提[Issue](https://gitee.com/openharmony/testfwk_arkxtest/issues) 给我们,当然,我们也非常欢迎你给我们提[PR](https://gitee.com/openharmony/testfwk_arkxtest/pulls) 。
|
||||
|
||||
## 开源协议
|
||||
|
||||
本项目基于 [Apache License 2.0](https://gitee.com/openharmony/testfwk_arkxtest/blob/master/hamock/LICENSE) ,请自由地享受和参与开源。
|
||||
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently.
|
||||
export { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently.
|
||||
export { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
58
oh_modules/.ohpm/@ohos+hamock@1.0.0/oh_modules/@ohos/hamock/index.d.ts
vendored
Normal file
58
oh_modules/.ohpm/@ohos+hamock@1.0.0/oh_modules/@ohos/hamock/index.d.ts
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export class ArgumentMatchers {
|
||||
static any;
|
||||
static anyString;
|
||||
static anyBoolean;
|
||||
static anyNumber;
|
||||
static anyObj;
|
||||
static anyFunction;
|
||||
static matchRegexs(Regex: RegExp): void
|
||||
}
|
||||
|
||||
declare interface when {
|
||||
afterReturn(value: any): any
|
||||
afterReturnNothing(): undefined
|
||||
afterAction(action: any): any
|
||||
afterThrow(e_msg: string): string
|
||||
(argMatchers?: any): when;
|
||||
}
|
||||
|
||||
export const when: when;
|
||||
|
||||
export interface VerificationMode {
|
||||
times(count: Number): void
|
||||
never(): void
|
||||
once(): void
|
||||
atLeast(count: Number): void
|
||||
atMost(count: Number): void
|
||||
}
|
||||
|
||||
export class MockKit {
|
||||
constructor()
|
||||
mockFunc(obj: Object, func: Function): Function
|
||||
mockObject(obj: Object): Object
|
||||
verify(methodName: String, argsArray: Array<any>): VerificationMode
|
||||
ignoreMock(obj: Object, func: Function): void
|
||||
clear(obj: Object): void
|
||||
clearAll(): void
|
||||
}
|
||||
|
||||
export declare function MockSetup(
|
||||
target: Object,
|
||||
propertyName: string | Symbol,
|
||||
descriptor: TypedPropertyDescriptor<() => void>
|
||||
): void;
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { MockSetup, MockKit, when } from './src/main/mock/MockKit';
|
||||
export { ArgumentMatchers } from './src/main/mock/ArgumentMatchers';
|
||||
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { MockSetup, MockKit, when } from './src/main/mock/MockKit.js';
|
||||
export { ArgumentMatchers } from './src/main/mock/ArgumentMatchers.js';
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { MockSetup, MockKit, when } from './src/main/mock/MockKit.js';
|
||||
export { ArgumentMatchers } from './src/main/mock/ArgumentMatchers.js';
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
{
|
||||
name: '@ohos/hamock',
|
||||
version: '1.0.0',
|
||||
description: 'A mock framework for OpenHarmony application.',
|
||||
main: 'index.ets',
|
||||
author: 'huawei',
|
||||
license: 'Apache-2.0',
|
||||
dependencies: {},
|
||||
ohos: {
|
||||
org: 'ohos',
|
||||
},
|
||||
types: 'index.d.ts'
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export class ArgumentMatchers {
|
||||
constructor() {
|
||||
this.ANY = "<any>";
|
||||
this.ANY_STRING = "<any String>";
|
||||
this.ANY_BOOLEAN = "<any Boolean>";
|
||||
this.ANY_NUMBER = "<any Number>";
|
||||
this.ANY_OBJECT = "<any Object>";
|
||||
this.ANY_FUNCTION = "<any Function>";
|
||||
this.MATCH_REGEXS = "<match regexs>";
|
||||
}
|
||||
static any() {
|
||||
}
|
||||
static anyString() {
|
||||
}
|
||||
static anyBoolean() {
|
||||
}
|
||||
static anyNumber() {
|
||||
}
|
||||
static anyObj() {
|
||||
}
|
||||
static anyFunction() {
|
||||
}
|
||||
static matchRegexs(regex) {
|
||||
if (ArgumentMatchers.isRegExp(regex)) {
|
||||
return regex;
|
||||
}
|
||||
throw Error("not a regex");
|
||||
}
|
||||
static isRegExp(value) {
|
||||
return Object.prototype.toString.call(value) === "[object RegExp]";
|
||||
}
|
||||
matcheReturnKey(...args) {
|
||||
let arg = args[0];
|
||||
let regex = args[1];
|
||||
let stubSetKey = args[2];
|
||||
if (stubSetKey && stubSetKey == this.ANY) {
|
||||
return this.ANY;
|
||||
}
|
||||
if (typeof arg === "string" && !regex) {
|
||||
return this.ANY_STRING;
|
||||
}
|
||||
if (typeof arg === "boolean" && !regex) {
|
||||
return this.ANY_BOOLEAN;
|
||||
}
|
||||
if (typeof arg === "number" && !regex) {
|
||||
return this.ANY_NUMBER;
|
||||
}
|
||||
if (typeof arg === "object" && !regex) {
|
||||
return this.ANY_OBJECT;
|
||||
}
|
||||
if (typeof arg === "function" && !regex) {
|
||||
return this.ANY_FUNCTION;
|
||||
}
|
||||
if (typeof arg === "string" && regex) {
|
||||
return regex.test(arg);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
matcheStubKey(key) {
|
||||
if (key === ArgumentMatchers.any) {
|
||||
return this.ANY;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyString) {
|
||||
return this.ANY_STRING;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyBoolean) {
|
||||
return this.ANY_BOOLEAN;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyNumber) {
|
||||
return this.ANY_NUMBER;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyObj) {
|
||||
return this.ANY_OBJECT;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyFunction) {
|
||||
return this.ANY_FUNCTION;
|
||||
}
|
||||
if (ArgumentMatchers.isRegExp(key)) {
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export class ArgumentMatchers {
|
||||
ANY = "<any>";
|
||||
ANY_STRING = "<any String>";
|
||||
ANY_BOOLEAN = "<any Boolean>";
|
||||
ANY_NUMBER = "<any Number>";
|
||||
ANY_OBJECT = "<any Object>";
|
||||
ANY_FUNCTION = "<any Function>";
|
||||
MATCH_REGEXS = "<match regexs>";
|
||||
|
||||
static any() {
|
||||
}
|
||||
|
||||
static anyString() {
|
||||
}
|
||||
|
||||
static anyBoolean() {
|
||||
}
|
||||
|
||||
static anyNumber() {
|
||||
}
|
||||
|
||||
static anyObj() {
|
||||
}
|
||||
|
||||
static anyFunction() {
|
||||
}
|
||||
|
||||
static matchRegexs(regex: any) {
|
||||
if (ArgumentMatchers.isRegExp(regex)) {
|
||||
return regex;
|
||||
}
|
||||
throw Error("not a regex");
|
||||
}
|
||||
|
||||
static isRegExp(value: string) {
|
||||
return Object.prototype.toString.call(value) === "[object RegExp]";
|
||||
}
|
||||
|
||||
matcheReturnKey(...args: Array<any>) {
|
||||
let arg = args[0];
|
||||
let regex = args[1];
|
||||
let stubSetKey = args[2];
|
||||
|
||||
if (stubSetKey && stubSetKey == this.ANY) {
|
||||
return this.ANY;
|
||||
}
|
||||
|
||||
if (typeof arg === "string" && !regex) {
|
||||
return this.ANY_STRING;
|
||||
}
|
||||
|
||||
if (typeof arg === "boolean" && !regex) {
|
||||
return this.ANY_BOOLEAN;
|
||||
}
|
||||
|
||||
if (typeof arg === "number" && !regex) {
|
||||
return this.ANY_NUMBER;
|
||||
}
|
||||
|
||||
if (typeof arg === "object" && !regex) {
|
||||
return this.ANY_OBJECT;
|
||||
}
|
||||
|
||||
if (typeof arg === "function" && !regex) {
|
||||
return this.ANY_FUNCTION;
|
||||
}
|
||||
|
||||
if (typeof arg === "string" && regex) {
|
||||
return regex.test(arg);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
matcheStubKey(key: any) {
|
||||
|
||||
if (key === ArgumentMatchers.any) {
|
||||
return this.ANY;
|
||||
}
|
||||
|
||||
if (key === ArgumentMatchers.anyString) {
|
||||
return this.ANY_STRING;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyBoolean) {
|
||||
return this.ANY_BOOLEAN;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyNumber) {
|
||||
return this.ANY_NUMBER;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyObj) {
|
||||
return this.ANY_OBJECT;
|
||||
}
|
||||
if (key === ArgumentMatchers.anyFunction) {
|
||||
return this.ANY_FUNCTION;
|
||||
}
|
||||
|
||||
if (ArgumentMatchers.isRegExp(key)) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
class ExtendInterface {
|
||||
constructor(mocker) {
|
||||
this.mocker = mocker;
|
||||
}
|
||||
stub() {
|
||||
this.params = arguments;
|
||||
return this;
|
||||
}
|
||||
stubMockedCall(returnInfo) {
|
||||
this.mocker.stubApply(this, this.params, returnInfo);
|
||||
}
|
||||
afterReturn(value) {
|
||||
this.stubMockedCall(function () {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
afterReturnNothing() {
|
||||
this.stubMockedCall(function () {
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
afterAction(action) {
|
||||
this.stubMockedCall(action);
|
||||
}
|
||||
afterThrow(msg) {
|
||||
this.stubMockedCall(function () {
|
||||
throw msg;
|
||||
});
|
||||
}
|
||||
clear(obj) {
|
||||
this.mocker.clear(obj);
|
||||
}
|
||||
}
|
||||
export default ExtendInterface;
|
||||
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockKit } from "./MockKit.js";
|
||||
|
||||
class ExtendInterface {
|
||||
|
||||
private mocker: MockKit
|
||||
private params: any
|
||||
|
||||
constructor(mocker: MockKit) {
|
||||
this.mocker = mocker;
|
||||
}
|
||||
|
||||
stub() {
|
||||
this.params = arguments;
|
||||
return this;
|
||||
}
|
||||
|
||||
stubMockedCall(returnInfo: any) {
|
||||
this.mocker.stubApply(this, this.params, returnInfo);
|
||||
}
|
||||
|
||||
afterReturn(value: any) {
|
||||
this.stubMockedCall(function () {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
afterReturnNothing() {
|
||||
this.stubMockedCall(function () {
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
afterAction(action: Function) {
|
||||
this.stubMockedCall(action);
|
||||
}
|
||||
|
||||
afterThrow(msg: string) {
|
||||
this.stubMockedCall(function () {
|
||||
throw msg;
|
||||
});
|
||||
}
|
||||
|
||||
clear(obj?: any) {
|
||||
this.mocker.clear(obj);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExtendInterface;
|
||||
@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import ExtendInterface from "./ExtendInterface.js";
|
||||
import VerificationMode from "./VerificationMode.js";
|
||||
import { ArgumentMatchers } from "./ArgumentMatchers.js";
|
||||
class MockKit {
|
||||
constructor() {
|
||||
this.mFunctions = [];
|
||||
this.stubs = new Map();
|
||||
this.recordCalls = new Map();
|
||||
this.currentSetKey = new Map();
|
||||
this.mockObj = null;
|
||||
this.recordMockedMethod = new Map();
|
||||
this.mFunctions = [];
|
||||
this.stubs = new Map();
|
||||
this.recordCalls = new Map();
|
||||
this.currentSetKey = new Map();
|
||||
this.mockObj = null;
|
||||
this.recordMockedMethod = new Map();
|
||||
}
|
||||
init() {
|
||||
this.reset();
|
||||
}
|
||||
reset() {
|
||||
this.mFunctions = [];
|
||||
this.stubs = new Map();
|
||||
this.recordCalls = new Map();
|
||||
this.currentSetKey = new Map();
|
||||
this.mockObj = null;
|
||||
this.recordMockedMethod = new Map();
|
||||
}
|
||||
clearAll() {
|
||||
this.reset();
|
||||
}
|
||||
clear(obj) {
|
||||
if (!obj) throw Error("Please enter an object to be cleaned");
|
||||
if (typeof (obj) !== 'object' && typeof (obj) !== 'function') throw new Error('Not a object or static class');
|
||||
this.recordMockedMethod.forEach(function (value, key, map) {
|
||||
if (key) {
|
||||
obj[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
ignoreMock(obj, method) {
|
||||
if (typeof (obj) !== 'object' && typeof (obj) !== 'function') throw new Error('Not a object or static class');
|
||||
if (typeof (method) !== 'function') throw new Error('Not a function');
|
||||
let og = this.recordMockedMethod.get(method.propName);
|
||||
if (og) {
|
||||
obj[method.propName] = og;
|
||||
this.recordMockedMethod.set(method.propName, undefined);
|
||||
}
|
||||
}
|
||||
extend(dest, source) {
|
||||
dest["stub"] = source["stub"];
|
||||
dest["afterReturn"] = source["afterReturn"];
|
||||
dest["afterReturnNothing"] = source["afterReturnNothing"];
|
||||
dest["afterAction"] = source["afterAction"];
|
||||
dest["afterThrow"] = source["afterThrow"];
|
||||
dest["stubMockedCall"] = source["stubMockedCall"];
|
||||
dest["clear"] = source["clear"];
|
||||
return dest;
|
||||
}
|
||||
stubApply(f, params, returnInfo) {
|
||||
let values = this.stubs.get(f);
|
||||
if (!values) {
|
||||
values = new Map();
|
||||
}
|
||||
let key = params[0];
|
||||
if (typeof key === "undefined") {
|
||||
key = "anonymous-mock-" + f.propName;
|
||||
}
|
||||
let matcher = new ArgumentMatchers();
|
||||
if (matcher.matcheStubKey(key)) {
|
||||
key = matcher.matcheStubKey(key);
|
||||
if (key) {
|
||||
this.currentSetKey.set(f, key);
|
||||
}
|
||||
}
|
||||
values.set(key, returnInfo);
|
||||
this.stubs.set(f, values);
|
||||
}
|
||||
getReturnInfo(f, params) {
|
||||
let values = this.stubs.get(f);
|
||||
if (!values) {
|
||||
return undefined;
|
||||
}
|
||||
let retrunKet = params[0];
|
||||
if (typeof retrunKet === "undefined") {
|
||||
retrunKet = "anonymous-mock-" + f.propName;
|
||||
}
|
||||
let stubSetKey = this.currentSetKey.get(f);
|
||||
|
||||
if (stubSetKey && (typeof (retrunKet) !== "undefined")) {
|
||||
retrunKet = stubSetKey;
|
||||
}
|
||||
let matcher = new ArgumentMatchers();
|
||||
if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) !== stubSetKey) {
|
||||
retrunKet = params[0];
|
||||
}
|
||||
values.forEach(function (value, key, map) {
|
||||
if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) {
|
||||
retrunKet = key;
|
||||
}
|
||||
});
|
||||
return values.get(retrunKet);
|
||||
}
|
||||
findName(obj, value) {
|
||||
let properties = this.findProperties(obj);
|
||||
let name = '';
|
||||
properties.filter((item) => (item !== 'caller' && item !== 'arguments')).forEach(function (va1, idx, array) {
|
||||
if (obj[va1] === value) {
|
||||
name = va1;
|
||||
}
|
||||
});
|
||||
return name;
|
||||
}
|
||||
isFunctionFromPrototype(f, container, propName) {
|
||||
if (container.constructor !== Object && container.constructor.prototype !== container) {
|
||||
return container.constructor.prototype[propName] === f;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
findProperties(obj, ...arg) {
|
||||
function getProperty(new_obj) {
|
||||
if (new_obj.__proto__ === null) {
|
||||
return [];
|
||||
}
|
||||
let properties = Object.getOwnPropertyNames(new_obj);
|
||||
return [...properties, ...getProperty(new_obj.__proto__)];
|
||||
}
|
||||
return getProperty(obj);
|
||||
}
|
||||
recordMethodCall(originalMethod, args) {
|
||||
originalMethod['getName'] = function () {
|
||||
return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
|
||||
};
|
||||
let name = originalMethod.getName();
|
||||
let arglistString = name + '(' + Array.from(args).toString() + ')';
|
||||
let records = this.recordCalls.get(arglistString);
|
||||
if (!records) {
|
||||
records = 0;
|
||||
}
|
||||
records++;
|
||||
this.recordCalls.set(arglistString, records);
|
||||
}
|
||||
mockFunc(originalObject, originalMethod) {
|
||||
let tmp = this;
|
||||
this.originalMethod = originalMethod;
|
||||
const _this = this;
|
||||
let f = function () {
|
||||
let args = arguments;
|
||||
let action = tmp.getReturnInfo(f, args);
|
||||
if (originalMethod) {
|
||||
tmp.recordMethodCall(originalMethod, args);
|
||||
}
|
||||
if (action) {
|
||||
return action.apply(_this, args);
|
||||
}
|
||||
};
|
||||
f.container = null || originalObject;
|
||||
f.original = originalMethod || null;
|
||||
if (originalObject && originalMethod) {
|
||||
if (typeof (originalMethod) != 'function')
|
||||
throw new Error('Not a function');
|
||||
var name = this.findName(originalObject, originalMethod);
|
||||
originalObject[name] = f;
|
||||
this.recordMockedMethod.set(name, originalMethod);
|
||||
f.propName = name;
|
||||
f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName);
|
||||
}
|
||||
f.mocker = this;
|
||||
this.mFunctions.push(f);
|
||||
this.extend(f, new ExtendInterface(this));
|
||||
return f;
|
||||
}
|
||||
verify(methodName, argsArray) {
|
||||
if (!methodName) {
|
||||
throw Error("not a function name");
|
||||
}
|
||||
let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
|
||||
return new VerificationMode(a ? a : 0);
|
||||
}
|
||||
mockObject(object) {
|
||||
if (!object || typeof object === "string") {
|
||||
throw Error(`this ${object} cannot be mocked`);
|
||||
}
|
||||
const _this = this;
|
||||
let mockedObject = {};
|
||||
let keys = Reflect.ownKeys(object);
|
||||
keys.filter(key => (typeof Reflect.get(object, key)) === 'function')
|
||||
.forEach((key) => {
|
||||
mockedObject[key] = object[key];
|
||||
mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
|
||||
});
|
||||
return mockedObject;
|
||||
}
|
||||
}
|
||||
function ifMockedFunction(f) {
|
||||
if (Object.prototype.toString.call(f) != "[object Function]" &&
|
||||
Object.prototype.toString.call(f) != "[object AsyncFunction]") {
|
||||
throw Error("not a function");
|
||||
}
|
||||
if (!f.stub) {
|
||||
throw Error("not a mock function");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function when(f) {
|
||||
if (ifMockedFunction(f)) {
|
||||
return f.stub.bind(f);
|
||||
}
|
||||
}
|
||||
function MockSetup(target, propertyName, descriptor) {
|
||||
const aboutToAppearOrigin = target.aboutToAppear;
|
||||
const setup = descriptor.value;
|
||||
target.aboutToAppear = function (...args) {
|
||||
if (target.__Param) { // copy attributes and params of the original context
|
||||
try {
|
||||
const map = target.__Param;
|
||||
for (const [key, val] of map) {
|
||||
this[key] = val; // 'this' refers to context of current function
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Mock setup param error: ${e}`);
|
||||
}
|
||||
}
|
||||
if (setup) { // apply the mock content
|
||||
try {
|
||||
setup.apply(this);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Mock setup apply error: ${e}`);
|
||||
}
|
||||
}
|
||||
if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context
|
||||
aboutToAppearOrigin.apply(this, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
export { MockSetup, MockKit, when };
|
||||
@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ExtendInterface from "./ExtendInterface.js";
|
||||
import VerificationMode from "./VerificationMode.js";
|
||||
import { ArgumentMatchers } from "./ArgumentMatchers.js";
|
||||
|
||||
interface IFunction extends Function {
|
||||
container: any;
|
||||
original: any;
|
||||
propName: string;
|
||||
originalFromPrototype: boolean
|
||||
mocker: MockKit
|
||||
}
|
||||
|
||||
class MockKit {
|
||||
|
||||
private mFunctions:Array<any> = [];
|
||||
private stubs = new Map();
|
||||
private recordCalls = new Map();
|
||||
private currentSetKey = new Map();
|
||||
private mockObj = null;
|
||||
private recordMockedMethod = new Map();
|
||||
private originalMethod: any;
|
||||
|
||||
constructor() {
|
||||
this.mFunctions = [];
|
||||
this.stubs = new Map();
|
||||
this.recordCalls = new Map();
|
||||
this.currentSetKey = new Map();
|
||||
this.mockObj = null;
|
||||
this.recordMockedMethod = new Map();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.mFunctions = [];
|
||||
this.stubs = new Map()
|
||||
this.recordCalls = new Map();
|
||||
this.currentSetKey = new Map();
|
||||
this.mockObj = null;
|
||||
this.recordMockedMethod = new Map();
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
clear(obj: any) {
|
||||
if (!obj) throw Error("Please enter an object to be cleaned");
|
||||
if (typeof (obj) != 'object') throw new Error('Not a object');
|
||||
this.recordMockedMethod.forEach(function (value, key, map) {
|
||||
if (key) {
|
||||
obj[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ignoreMock(obj:any, method: any) {
|
||||
if (typeof (obj) != 'object') throw new Error('Not a object');
|
||||
if (typeof (method) != 'function') throw new Error('Not a function');
|
||||
let og = this.recordMockedMethod.get(method.propName);
|
||||
if (og) {
|
||||
obj[method.propName] = og;
|
||||
this.recordMockedMethod.set(method.propName, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
extend(dest: any, source:any) {
|
||||
dest["stub"] = source["stub"];
|
||||
dest["afterReturn"] = source["afterReturn"];
|
||||
dest["afterReturnNothing"] = source["afterReturnNothing"];
|
||||
dest["afterAction"] = source["afterAction"];
|
||||
dest["afterThrow"] = source["afterThrow"];
|
||||
dest["stubMockedCall"] = source["stubMockedCall"];
|
||||
dest["clear"] = source["clear"];
|
||||
return dest;
|
||||
}
|
||||
|
||||
stubApply(f: any, params:any, returnInfo:any) {
|
||||
let values = this.stubs.get(f);
|
||||
if (!values) {
|
||||
values = new Map();
|
||||
}
|
||||
let key = params[0];
|
||||
if (typeof key == "undefined") {
|
||||
key = "anonymous-mock-" + f.propName;
|
||||
}
|
||||
let matcher = new ArgumentMatchers();
|
||||
if (matcher.matcheStubKey(key)) {
|
||||
key = matcher.matcheStubKey(key);
|
||||
if (key) {
|
||||
this.currentSetKey.set(f, key);
|
||||
}
|
||||
}
|
||||
values.set(key, returnInfo);
|
||||
this.stubs.set(f, values);
|
||||
}
|
||||
|
||||
getReturnInfo(f: any, params:any) {
|
||||
let values = this.stubs.get(f);
|
||||
if (!values) {
|
||||
return undefined;
|
||||
}
|
||||
let retrunKet = params[0];
|
||||
if (typeof retrunKet == "undefined") {
|
||||
retrunKet = "anonymous-mock-" + f.propName;
|
||||
}
|
||||
let stubSetKey = this.currentSetKey.get(f);
|
||||
|
||||
if (stubSetKey && (typeof (retrunKet) != "undefined")) {
|
||||
retrunKet = stubSetKey;
|
||||
}
|
||||
let matcher = new ArgumentMatchers();
|
||||
if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) != stubSetKey) {
|
||||
retrunKet = params[0];
|
||||
}
|
||||
|
||||
values.forEach(function (value: any, key: any, map: any) {
|
||||
if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) {
|
||||
retrunKet = key;
|
||||
}
|
||||
});
|
||||
|
||||
return values.get(retrunKet);
|
||||
}
|
||||
|
||||
findName(obj: any, value: any) {
|
||||
let properties = this.findProperties(obj);
|
||||
let name = '';
|
||||
properties.filter((item:any) => (item !== 'caller' && item !== 'arguments')).forEach(
|
||||
function (va1:any, idx:any, array:any) {
|
||||
if (obj[va1] === value) {
|
||||
name = va1;
|
||||
}
|
||||
}
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
isFunctionFromPrototype(f: Function, container:Function, propName: string) {
|
||||
if (container.constructor != Object && container.constructor.prototype !== container) {
|
||||
return container.constructor.prototype[propName] === f;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
findProperties(obj: any, ...arg: Array<any>) {
|
||||
function getProperty(new_obj:any): Array<any> {
|
||||
if (new_obj.__proto__ === null) {
|
||||
return [];
|
||||
}
|
||||
let properties = Object.getOwnPropertyNames(new_obj);
|
||||
return [...properties, ...getProperty(new_obj.__proto__)];
|
||||
}
|
||||
return getProperty(obj);
|
||||
}
|
||||
|
||||
recordMethodCall(originalMethod: any, args: any) {
|
||||
originalMethod['getName'] = function () {
|
||||
return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
|
||||
}
|
||||
let name = originalMethod.getName();
|
||||
let arglistString = name + '(' + Array.from(args).toString() + ')';
|
||||
let records = this.recordCalls.get(arglistString);
|
||||
if (!records) {
|
||||
records = 0;
|
||||
}
|
||||
records++;
|
||||
this.recordCalls.set(arglistString, records);
|
||||
}
|
||||
|
||||
mockFunc(originalObject:any, originalMethod:any) {
|
||||
let tmp = this;
|
||||
this.originalMethod = originalMethod;
|
||||
const _this = this;
|
||||
let f:any = function () {
|
||||
let args = arguments;
|
||||
let action = tmp.getReturnInfo(f, args);
|
||||
if (originalMethod) {
|
||||
tmp.recordMethodCall(originalMethod, args);
|
||||
}
|
||||
if (action) {
|
||||
return <IFunction> action.apply(_this, args);
|
||||
}
|
||||
};
|
||||
|
||||
f.container = null || originalObject;
|
||||
f.original = originalMethod || null;
|
||||
|
||||
if (originalObject && originalMethod) {
|
||||
if (typeof (originalMethod) != 'function') throw new Error('Not a function');
|
||||
var name = this.findName(originalObject, originalMethod);
|
||||
originalObject[name] = f;
|
||||
this.recordMockedMethod.set(name, originalMethod);
|
||||
f.propName = name;
|
||||
f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName);
|
||||
}
|
||||
f.mocker = this;
|
||||
this.mFunctions.push(f);
|
||||
this.extend(f, new ExtendInterface(this));
|
||||
return f;
|
||||
}
|
||||
|
||||
verify(methodName:any, argsArray:any) {
|
||||
if (!methodName) {
|
||||
throw Error("not a function name");
|
||||
}
|
||||
let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
|
||||
return new VerificationMode(a ? a : 0);
|
||||
}
|
||||
|
||||
mockObject(object: any) {
|
||||
if (!object || typeof object === "string") {
|
||||
throw Error(`this ${object} cannot be mocked`);
|
||||
}
|
||||
const _this = this;
|
||||
let mockedObject:any = {};
|
||||
let keys = Reflect.ownKeys(object);
|
||||
keys.filter(key => (typeof Reflect.get(object, key)) === 'function')
|
||||
.forEach((key:any) => {
|
||||
mockedObject[key] = object[key];
|
||||
mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
|
||||
});
|
||||
return mockedObject;
|
||||
}
|
||||
}
|
||||
|
||||
function ifMockedFunction(f: any) {
|
||||
if (Object.prototype.toString.call(f) != "[object Function]" &&
|
||||
Object.prototype.toString.call(f) != "[object AsyncFunction]") {
|
||||
throw Error("not a function");
|
||||
}
|
||||
if (!f.stub) {
|
||||
throw Error("not a mock function");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function when(f: any) {
|
||||
if (ifMockedFunction(f)) {
|
||||
return f.stub.bind(f);
|
||||
}
|
||||
}
|
||||
|
||||
function MockSetup(target: Object, propertyName: string | Symbol, descriptor: TypedPropertyDescriptor<() => void>): void {
|
||||
const aboutToAppearOrigin = target.aboutToAppear;
|
||||
const setup = descriptor.value;
|
||||
target.aboutToAppear = function (...args: any[]) {
|
||||
if (target.__Param) { // copy attributes and params of the original context
|
||||
try {
|
||||
const map = target.__Param as Map<string, unknown>;
|
||||
for (const [key, val] of map) {
|
||||
this[key] = val; // 'this' refers to context of current function
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Mock setup param error: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (setup) { // apply the mock content
|
||||
try {
|
||||
setup.apply(this);
|
||||
} catch (e) {
|
||||
throw new Error(`Mock setup apply error: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context
|
||||
aboutToAppearOrigin.apply(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
MockSetup,
|
||||
MockKit,
|
||||
when
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
class VerificationMode {
|
||||
constructor(times) {
|
||||
this.doTimes = times;
|
||||
}
|
||||
times(count) {
|
||||
if (count !== this.doTimes) {
|
||||
throw Error(`expect ${count} actual ${this.doTimes}`);
|
||||
}
|
||||
}
|
||||
never() {
|
||||
if (this.doTimes !== 0) {
|
||||
throw Error(`expect 0 actual ${this.doTimes}`);
|
||||
}
|
||||
}
|
||||
once() {
|
||||
if (this.doTimes !== 1) {
|
||||
throw Error(`expect 1 actual ${this.doTimes}`);
|
||||
}
|
||||
}
|
||||
atLeast(count) {
|
||||
if (count > this.doTimes) {
|
||||
throw Error('failed ' + count + ' greater than the actual execution times of method');
|
||||
}
|
||||
}
|
||||
atMost(count) {
|
||||
if (count < this.doTimes) {
|
||||
throw Error('failed ' + count + ' less than the actual execution times of method');
|
||||
}
|
||||
}
|
||||
}
|
||||
export default VerificationMode;
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
class VerificationMode {
|
||||
|
||||
private doTimes: number
|
||||
|
||||
constructor(times: number) {
|
||||
this.doTimes = times;
|
||||
}
|
||||
|
||||
times(count: number) {
|
||||
if(count !== this.doTimes) {
|
||||
throw Error(`expect ${count} actual ${this.doTimes}`);
|
||||
}
|
||||
}
|
||||
|
||||
never() {
|
||||
if (this.doTimes !== 0) {
|
||||
throw Error(`expect 0 actual ${this.doTimes}`);
|
||||
}
|
||||
}
|
||||
|
||||
once() {
|
||||
if (this.doTimes !== 1) {
|
||||
throw Error(`expect 1 actual ${this.doTimes}`);
|
||||
}
|
||||
}
|
||||
|
||||
atLeast(count: number) {
|
||||
if (count > this.doTimes) {
|
||||
throw Error('failed ' + count + ' greater than the actual execution times of method');
|
||||
}
|
||||
}
|
||||
|
||||
atMost(count: number) {
|
||||
if (count < this.doTimes) {
|
||||
throw Error('failed ' + count + ' less than the actual execution times of method');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VerificationMode;
|
||||
@ -0,0 +1,22 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.example.hamock",
|
||||
"debug": true,
|
||||
"versionCode": 1000000,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 9,
|
||||
"targetAPIVersion": 9,
|
||||
"apiReleaseType": "Release"
|
||||
},
|
||||
"module": {
|
||||
"name": "hamock",
|
||||
"type": "har",
|
||||
"deviceTypes": [
|
||||
"default",
|
||||
"tablet",
|
||||
"tv",
|
||||
"wearable",
|
||||
"car"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "JSON schema for mock-config.json5 file",
|
||||
"definitions": {
|
||||
"sourceRedirection": {
|
||||
"description": "A source redirection for mocked module.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patternProperties": {
|
||||
".+": {
|
||||
"$ref": "#/definitions/sourceRedirection"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||
*/
|
||||
export const HAR_VERSION = '1.0.24';
|
||||
export const BUILD_MODE_NAME = 'debug';
|
||||
export const DEBUG = true;
|
||||
export const TARGET_NAME = 'default';
|
||||
|
||||
/**
|
||||
* BuildProfile Class is used only for compatibility purposes.
|
||||
*/
|
||||
export default class BuildProfile {
|
||||
static readonly HAR_VERSION = HAR_VERSION;
|
||||
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||
static readonly DEBUG = DEBUG;
|
||||
static readonly TARGET_NAME = TARGET_NAME;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
### 1.0.24
|
||||
- 提示信息优化
|
||||
### 1.0.23
|
||||
- 断言错误提示信息优化
|
||||
### 1.0.22
|
||||
- mock五参数失败问题修复
|
||||
### 1.0.21
|
||||
- mock支持多参数
|
||||
- describe中异步函数抛出日志信息
|
||||
- 修复多测试套时,执行单个测试套会打印其他测试套的日志信息
|
||||
## 1.0.14
|
||||
- 堆栈信息打印到cmd
|
||||
## 1.0.15
|
||||
- 支持获取测试代码的失败堆栈信息
|
||||
- mock代码迁移至harmock包
|
||||
- 适配arkts语法
|
||||
- 修复覆盖率数据容易截断的bug
|
||||
## 1.0.16
|
||||
- 修改覆盖率文件生成功能
|
||||
- 修改静态方法无法ignoreMock函数
|
||||
## 1.0.17
|
||||
- 修改not断言失败提示日志
|
||||
- 自定义错误message信息
|
||||
- 添加xdescribe, xit API功能
|
||||
## 1.0.18
|
||||
- 添加全局变量存储API get set
|
||||
- 自定义断言功能
|
||||
## 1.0.18-rc.0
|
||||
添加框架worker执行能力
|
||||
## 1.0.19
|
||||
规范日志格式
|
||||
# 1.0.20
|
||||
代码告警整改
|
||||
@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@ -0,0 +1,229 @@
|
||||
<div style="text-align: center;font-size: xxx-large" >Hypium</div>
|
||||
<div style="text-align: center">A unit test framework for OpenHarmonyOS application</div>
|
||||
|
||||
## Hypium是什么?
|
||||
***
|
||||
- Hypium是OpenHarmony上的测试框架,提供测试用例编写、执行、结果显示能力,用于OpenHarmony系统应用接口以及应用界面测试。
|
||||
- Hypium结构化模型:hypium工程主要由List.test.js与TestCase.test.js组成。
|
||||
```
|
||||
rootProject // Hypium工程根目录
|
||||
├── moduleA
|
||||
│ ├── src
|
||||
│ ├── main // 被测试应用目录
|
||||
│ ├── ohosTest // 测试用例目录
|
||||
│ ├── js/ets
|
||||
│ └── test
|
||||
│ └── List.test.js // 测试用例加载脚本,ets目录下为.ets后缀
|
||||
│ └── TestCase.test.js // 测试用例脚本,ets目录下为.ets后缀
|
||||
└── moduleB
|
||||
...
|
||||
│ └── List.test.js // 测试用例加载脚本,ets目录下为.ets后缀
|
||||
│ └── TestCase.test.js // 测试用例脚本,ets目录下为.ets后缀
|
||||
```
|
||||
|
||||
## 安装使用
|
||||
|
||||
- 方式一
|
||||
```javascript
|
||||
ohpm i @ohos/hypium
|
||||
```
|
||||
- 方式二
|
||||
***
|
||||
- 在DevEco Studio内使用Hypium
|
||||
- 工程级oh-package.json5内配置:
|
||||
```json
|
||||
"dependencies": {
|
||||
"@ohos/hypium": "1.0.24"
|
||||
}
|
||||
```
|
||||
注:
|
||||
hypium服务于OpenHarmonyOS应用对外接口测试、系统对外接口测试(SDK中接口),完成HAP自动化测试。详细指导:
|
||||
[Deveco Studio](https://developer.harmonyos.com/cn/develop/deveco-studio)
|
||||
|
||||
#### 通用语法
|
||||
|
||||
- 测试用例采用业内通用语法,describe代表一个测试套, it代表一条用例。
|
||||
|
||||
| No. | API | 功能说明 |
|
||||
|-----| ----------------- |------------------------------------------------------------------------|
|
||||
| 1 | describe | 定义一个测试套,支持两个参数:测试套名称和测试套函数。其中测试套函数不能是异步函数 |
|
||||
| 2 | beforeAll | 在测试套内定义一个预置条件,在所有测试用例开始前执行且仅执行一次,支持一个参数:预置动作函数。 |
|
||||
| 3 | beforeEach | 在测试套内定义一个单元预置条件,在每条测试用例开始前执行,执行次数与it定义的测试用例数一致,支持一个参数:预置动作函数。 |
|
||||
| 4 | afterEach | 在测试套内定义一个单元清理条件,在每条测试用例结束后执行,执行次数与it定义的测试用例数一致,支持一个参数:清理动作函数。 |
|
||||
| 5 | afterAll | 在测试套内定义一个清理条件,在所有测试用例结束后执行且仅执行一次,支持一个参数:清理动作函数。 |
|
||||
| 6 | beforeItSpecified | @since1.0.15在测试套内定义一个单元预置条件,仅在指定测试用例开始前执行,支持两个参数:单个用例名称或用例名称数组、预置动作函数。 |
|
||||
| 7 | afterItSpecified | @since1.0.15在测试套内定义一个单元清理条件,仅在指定测试用例结束后执行,支持两个参数:单个用例名称或用例名称数组、清理动作函数 |
|
||||
| 8 | it | 定义一条测试用例,支持三个参数:用例名称,过滤参数和用例函数。 |
|
||||
| 9 | expect | 支持bool类型判断等多种断言方法。 |
|
||||
| 10 | xdescribe | @since1.0.17定义一个跳过的测试套,支持两个参数:测试套名称和测试套函数。 |
|
||||
| 11 | xit | @since1.0.17定义一条跳过的测试用例,支持三个参数:用例名称,过滤参数和用例函数。 | |
|
||||
|
||||
#### 断言库
|
||||
|
||||
- 示例代码:
|
||||
|
||||
```javascript
|
||||
expect(${actualvalue}).assertX(${expectvalue})
|
||||
```
|
||||
|
||||
- 断言功能列表:
|
||||
|
||||
| No. | API | 功能说明 |
|
||||
| :--- | :------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| 1 | assertClose | 检验actualvalue和expectvalue(0)的接近程度是否是expectValue(1) |
|
||||
| 2 | assertContain | 检验actualvalue中是否包含expectvalue |
|
||||
| 3 | assertDeepEquals | @since1.0.4 检验actualvalue和expectvalue(0)是否是同一个对象 |
|
||||
| 4 | assertEqual | 检验actualvalue是否等于expectvalue[0] |
|
||||
| 5 | assertFail | 抛出一个错误 |
|
||||
| 6 | assertFalse | 检验actualvalue是否是false |
|
||||
| 7 | assertTrue | 检验actualvalue是否是true |
|
||||
| 8 | assertInstanceOf | 检验actualvalue是否是expectvalue类型 |
|
||||
| 9 | assertLarger | 检验actualvalue是否大于expectvalue |
|
||||
| 10 | assertLess | 检验actualvalue是否小于expectvalue |
|
||||
| 11 | assertNaN | @since1.0.4 检验actualvalue是否是NaN |
|
||||
| 12 | assertNegUnlimited | @since1.0.4 检验actualvalue是否等于Number.NEGATIVE_INFINITY |
|
||||
| 13 | assertNull | 检验actualvalue是否是null |
|
||||
| 14 | assertPosUnlimited | @since1.0.4 检验actualvalue是否等于Number.POSITIVE_INFINITY |
|
||||
| 15 | assertPromiseIsPending | @since1.0.4 检验actualvalue是否处于Pending状态【actualvalue为promse对象】 |
|
||||
| 16 | assertPromiseIsRejected | @since1.0.4 检验actualvalue是否处于Rejected状态【同15】 |
|
||||
| 17 | assertPromiseIsRejectedWith | @since1.0.4 检验actualvalue是否处于Rejected状态,并且比较执行的结果值【同15】 |
|
||||
| 18 | assertPromiseIsRejectedWithError | @since1.0.4 检验actualvalue是否处于Rejected状态并有异常,同时比较异常的类型和message值【同15】 |
|
||||
| 19 | assertPromiseIsResolved | @since1.0.4 检验actualvalue是否处于Resolved状态【同15】 |
|
||||
| 20 | assertPromiseIsResolvedWith | @since1.0.4 检验actualvalue是否处于Resolved状态,并且比较执行的结果值【同15】 |
|
||||
| 21 | assertThrowError | 检验actualvalue抛出Error内容是否是expectValue |
|
||||
| 22 | assertUndefined | 检验actualvalue是否是undefined |
|
||||
| 23 | not | @since1.0.4 断言结果取反 |
|
||||
| 24 | message | @since1.0.17自定义断言异常信息 |
|
||||
|
||||
示例代码:
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from '@ohos/hypium';
|
||||
|
||||
export default async function assertCloseTest() {
|
||||
describe('assertClose', function () {
|
||||
it('assertClose_success', 0, function () {
|
||||
let a = 100;
|
||||
let b = 0.1;
|
||||
expect(a).assertClose(99, b);
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 公共系统能力
|
||||
|
||||
| No. | API | 功能描述 |
|
||||
| ---- | ------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| 1 | existKeyword(keyword: string, timeout: number): boolean | @since1.0.3 hilog日志中查找指定字段是否存在,keyword是待查找关键字,timeout为设置的查找时间 |
|
||||
| 2 | actionStart(tag: string): void | @since1.0.3 cmd窗口输出开始tag |
|
||||
| 3 | actionEnd(tag: string): void | @since1.0.3 cmd窗口输出结束tag |
|
||||
|
||||
示例代码:
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect, SysTestKit} from '@ohos/hypium';
|
||||
|
||||
export default function existKeywordTest() {
|
||||
describe('existKeywordTest', function () {
|
||||
it('existKeyword',DEFAULT, async function () {
|
||||
console.info("HelloTest");
|
||||
let isExist = await SysTestKit.existKeyword('HelloTest');
|
||||
console.info('isExist ------>' + isExist);
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
```javascript
|
||||
import { describe, it, expect, SysTestKit} from '@ohos/hypium';
|
||||
|
||||
export default function actionTest() {
|
||||
describe('actionTest', function () {
|
||||
it('existKeyword',DEFAULT, async function () {
|
||||
let tag = '[MyTest]';
|
||||
SysTestKit.actionStart(tag);
|
||||
//do something
|
||||
SysTestKit.actionEnd(tag);
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 专项能力
|
||||
|
||||
- 测试用例属性筛选能力:hypium支持根据用例属性筛选执行指定测试用例,使用方式是先在测试用例上标记用例属性后,再在测试应用的启动shell命令后新增" -s ${Key} ${Value}"。
|
||||
|
||||
| Key | 含义说明 | Value取值范围 |
|
||||
| -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| level | 用例级别 | "0","1","2","3","4", 例如:-s level 1 |
|
||||
| size | 用例粒度 | "small","medium","large", 例如:-s size small |
|
||||
| testType | 用例测试类型 | "function","performance","power","reliability","security","global","compatibility","user","standard","safety","resilience", 例如:-s testType function |
|
||||
|
||||
示例代码
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect, TestType, Size, Level } from '@ohos/hypium';
|
||||
|
||||
export default function attributeTest() {
|
||||
describe('attributeTest', function () {
|
||||
it("testAttributeIt", TestType.FUNCTION | Size.SMALLTEST | Level.LEVEL0, function () {
|
||||
console.info('Hello Test');
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
示例命令
|
||||
```shell
|
||||
XX -s level 1 -s size small -s testType function
|
||||
```
|
||||
该命令的作用是:筛选测试应用中同时满足a)用例级别是1 b)用例粒度是small c)用例测试类型是function 三个条件的用例执行。
|
||||
|
||||
- 测试套/测试用例名称筛选能力(测试套与用例名称用“#”号连接,多个用“,”英文逗号分隔)
|
||||
|
||||
| Key | 含义说明 | Value取值范围 |
|
||||
| -------- | ----------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| class | 指定要执行的测试套&用例 | ${describeName}#${itName},${describeName} , 例如:-s class attributeTest#testAttributeIt |
|
||||
| notClass | 指定不执行的测试套&用例 | ${describeName}#${itName},${describeName} , 例如:-s notClass attributeTest#testAttributeIt |
|
||||
|
||||
示例命令
|
||||
```shell
|
||||
XX -s class attributeTest#testAttributeIt,abilityTest#testAbilityIt
|
||||
```
|
||||
该命令的作用是:筛选测试应用中attributeTest测试套下的testAttributeIt测试用例,abilityTest测试套下的testAbilityIt测试用例,只执行这两条用例。
|
||||
|
||||
- 其他能力
|
||||
|
||||
| 能力项 | Key | 含义说明 | Value取值范围 |
|
||||
| ------------ | ------- | ---------------------------- | ---------------------------------------------- |
|
||||
| 随机执行能力 | random | 测试套&测试用例随机执行 | true, 不传参默认为false, 例如:-s random true |
|
||||
| 空跑能力 | dryRun | 显示要执行的测试用例信息全集 | true , 不传参默认为false,例如:-s dryRun true |
|
||||
| 异步超时能力 | timeout | 异步用例执行的超时时间 | 正整数 , 单位ms,例如:-s timeout 5000 |
|
||||
|
||||
##### 约束限制
|
||||
随机执行能力和空跑能力从npm包1.0.3版本开始支持
|
||||
|
||||
#### Mock能力
|
||||
|
||||
##### 约束限制
|
||||
|
||||
单元测试框架Mock能力从npm包[1.0.1版本](https://repo.harmonyos.com/#/cn/application/atomService/@ohos%2Fhypium/v/1.0.1)开始支持
|
||||
|
||||
## 约束
|
||||
|
||||
***
|
||||
本模块首批接口从OpenHarmony SDK API version 8开始支持。
|
||||
|
||||
## Hypium开放能力隐私声明
|
||||
|
||||
- 我们如何收集和使用您的个人信息
|
||||
您在使用集成了Hypium开放能力的测试应用时,Hypium不会处理您的个人信息。
|
||||
- SDK处理的个人信息
|
||||
不涉及。
|
||||
- SDK集成第三方服务声明
|
||||
不涉及。
|
||||
- SDK数据安全保护
|
||||
不涉及。
|
||||
- SDK版本更新声明
|
||||
为了向您提供最新的服务,我们会不时更新Hypium版本。我们强烈建议开发者集成使用最新版本的Hypium。
|
||||
|
||||
@ -1,41 +1,31 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
"resOptions": {
|
||||
"copyCodeResource": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"buildOptionSet": [
|
||||
{
|
||||
"name": "release",
|
||||
"arkOptions": {
|
||||
"obfuscation": {
|
||||
"ruleOptions": {
|
||||
"enable": false,
|
||||
"files": [
|
||||
"./obfuscation-rules.txt"
|
||||
]
|
||||
},
|
||||
"consumerFiles": [
|
||||
"./consumer-rules.txt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"resOptions": {
|
||||
"copyCodeResource": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
},
|
||||
{
|
||||
"name": "ohosTest"
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"buildOptionSet": [
|
||||
{
|
||||
"name": "release",
|
||||
"arkOptions": {
|
||||
"obfuscation": {
|
||||
"ruleOptions": {
|
||||
"enable": false,
|
||||
"files": [
|
||||
"./obfuscation-rules.txt"
|
||||
]
|
||||
},
|
||||
"consumerFiles": [
|
||||
"./consumer-rules.txt"
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
},
|
||||
{
|
||||
"name": "ohosTest"
|
||||
}
|
||||
]
|
||||
}
|
||||
150
oh_modules/.ohpm/@ohos+hypium@1.0.24/oh_modules/@ohos/hypium/index.d.ts
vendored
Normal file
150
oh_modules/.ohpm/@ohos+hypium@1.0.24/oh_modules/@ohos/hypium/index.d.ts
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const DEFAULT = 0B0000
|
||||
|
||||
export const when: when;
|
||||
|
||||
export enum TestType {
|
||||
FUNCTION = 0B1,
|
||||
PERFORMANCE = 0B1 << 1,
|
||||
POWER = 0B1 << 2,
|
||||
RELIABILITY = 0B1 << 3,
|
||||
SECURITY = 0B1 << 4,
|
||||
GLOBAL = 0B1 << 5,
|
||||
COMPATIBILITY = 0B1 << 6,
|
||||
USER = 0B1 << 7,
|
||||
STANDARD = 0B1 << 8,
|
||||
SAFETY = 0B1 << 9,
|
||||
RESILIENCE = 0B1 << 10
|
||||
}
|
||||
|
||||
export enum Size {
|
||||
SMALLTEST = 0B1 << 16,
|
||||
MEDIUMTEST = 0B1 << 17,
|
||||
LARGETEST = 0B1 << 18
|
||||
}
|
||||
|
||||
export enum Level {
|
||||
LEVEL0 = 0B1 << 24,
|
||||
LEVEL1 = 0B1 << 25,
|
||||
LEVEL2 = 0B1 << 26,
|
||||
LEVEL3 = 0B1 << 27,
|
||||
LEVEL4 = 0B1 << 28
|
||||
}
|
||||
export { xdescribe, xit, describe, it } from './index';
|
||||
|
||||
|
||||
|
||||
export function beforeItSpecified(testCaseNames: Array<string> | string, callback: Function): void
|
||||
|
||||
export function afterItSpecified(testCaseNames: Array<string> | string, callback: Function): void
|
||||
|
||||
export function beforeEach(callback: Function): void
|
||||
|
||||
export function afterEach(callback: Function): void
|
||||
|
||||
export function beforeAll(callback: Function): void
|
||||
|
||||
export function afterAll(callback: Function): void
|
||||
|
||||
|
||||
export interface Assert {
|
||||
assertClose(expectValue: number, precision: number): void
|
||||
assertContain(expectValue: any): void
|
||||
assertEqual(expectValue: any): void
|
||||
assertFail(): void
|
||||
assertFalse(): void
|
||||
assertTrue(): void
|
||||
assertInstanceOf(expectValue: string): void
|
||||
assertLarger(expectValue: number): void
|
||||
assertLess(expectValue: number): void
|
||||
assertNull(): void
|
||||
assertThrowError(expectValue: string | Function): void
|
||||
assertUndefined(): void
|
||||
assertLargerOrEqual(expectValue: number): void
|
||||
assertLessOrEqual(expectValue: number): void
|
||||
assertNaN(): void
|
||||
assertNegUnlimited(): void
|
||||
assertPosUnlimited(): void
|
||||
not(): Assert;
|
||||
assertDeepEquals(expectValue: any): void
|
||||
assertPromiseIsPending(): Promise<void>
|
||||
assertPromiseIsRejected(): Promise<void>
|
||||
assertPromiseIsRejectedWith(expectValue?: any): Promise<void>
|
||||
assertPromiseIsRejectedWithError(...expectValue): Promise<void>
|
||||
assertPromiseIsResolved(): Promise<void>
|
||||
assertPromiseIsResolvedWith(expectValue?: any): Promise<void>
|
||||
message(msg: string): Assert
|
||||
}
|
||||
|
||||
export function expect(actualValue?: any): Assert
|
||||
|
||||
export class ArgumentMatchers {
|
||||
static any;
|
||||
static anyString;
|
||||
static anyBoolean;
|
||||
static anyNumber;
|
||||
static anyObj;
|
||||
static anyFunction;
|
||||
static matchRegexs(Regex: RegExp): void
|
||||
}
|
||||
|
||||
declare interface when {
|
||||
afterReturn(value: any): any
|
||||
afterReturnNothing(): undefined
|
||||
afterAction(action: any): any
|
||||
afterThrow(e_msg: string): string
|
||||
(argMatchers?: any): when;
|
||||
}
|
||||
|
||||
export interface VerificationMode {
|
||||
times(count: Number): void
|
||||
never(): void
|
||||
once(): void
|
||||
atLeast(count: Number): void
|
||||
atMost(count: Number): void
|
||||
}
|
||||
|
||||
export class MockKit {
|
||||
constructor()
|
||||
mockFunc(obj: Object, func: Function): Function
|
||||
mockObject(obj: Object): Object
|
||||
verify(methodName: String, argsArray: Array<any>): VerificationMode
|
||||
ignoreMock(obj: Object, func: Function): void
|
||||
clear(obj: Object): void
|
||||
clearAll(): void
|
||||
}
|
||||
|
||||
export class SysTestKit {
|
||||
static getDescribeName(): string;
|
||||
static getItName(): string;
|
||||
static getItAttribute(): TestType | Size | Level
|
||||
static actionStart(tag: string): void
|
||||
static actionEnd(tag: string): void
|
||||
static existKeyword(keyword: string, timeout?: number): boolean
|
||||
}
|
||||
|
||||
export class Hypium {
|
||||
static setData(data: { [key: string]: any }): void
|
||||
static setTimeConfig(systemTime: any)
|
||||
static hypiumTest(abilityDelegator: any, abilityDelegatorArguments: any, testsuite: Function): void
|
||||
static set(key: string, value: any): void
|
||||
static get(key: string): any
|
||||
static registerAssert(customAssertion: Function): void
|
||||
static unregisterAssert(customAssertion: string | Function): void
|
||||
static hypiumWorkerTest(abilityDelegator: Object, abilityDelegatorArguments: Object, testsuite: Function, workerPort: Object): void;
|
||||
static hypiumInitWorkers(abilityDelegator: Object, scriptURL: string, workerNum: number, params: Object): void;
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Core from './src/main/core';
|
||||
import {TestType, Size, Level, DEFAULT} from './src/main/Constant';
|
||||
import DataDriver from './src/main/module/config/DataDriver';
|
||||
import ExpectExtend from './src/main/module/assert/ExpectExtend';
|
||||
import OhReport from './src/main/module/report/OhReport';
|
||||
export { xdescribe, xit, describe, it } from './index.ts';
|
||||
|
||||
export declare class Hypium {
|
||||
static setData(data: Object): void
|
||||
static setTimeConfig(systemTime: Object): void
|
||||
static hypiumTest(abilityDelegator: Object, abilityDelegatorArguments: Object, testsuite: Function): void
|
||||
static set(key: string, value: Object): void
|
||||
static get(key: string): Object
|
||||
static registerAssert(customAssertion: Function): void
|
||||
static unregisterAssert(customAssertion: string | Function): void
|
||||
static hypiumWorkerTest(abilityDelegator: Object, abilityDelegatorArguments: Object,
|
||||
testsuite: Function, workerPort: Object): void;
|
||||
static hypiumInitWorkers(abilityDelegator: Object, scriptURL: string, workerNum: number, params: Object): void;
|
||||
}
|
||||
|
||||
export {
|
||||
Core,
|
||||
DataDriver,
|
||||
ExpectExtend,
|
||||
OhReport,
|
||||
TestType,
|
||||
Size,
|
||||
Level,
|
||||
DEFAULT
|
||||
};
|
||||
|
||||
type allExpectType = Object | undefined | null
|
||||
|
||||
export declare function beforeItSpecified(testCaseNames: Array<string> | string, callback: Function): void
|
||||
|
||||
export declare function afterItSpecified(testCaseNames: Array<string> | string, callback: Function): void
|
||||
|
||||
export declare function beforeEach(callback: Function): void
|
||||
|
||||
export declare function afterEach(callback: Function): void
|
||||
|
||||
export declare function beforeAll(callback: Function): void
|
||||
|
||||
export declare function afterAll(callback: Function): void
|
||||
|
||||
export declare interface Assert {
|
||||
assertClose(expectValue: number, precision: number): void
|
||||
assertContain(expectValue: allExpectType): void
|
||||
assertEqual(expectValue: allExpectType): void
|
||||
assertFail(): void
|
||||
assertFalse(): void
|
||||
assertTrue(): void
|
||||
assertInstanceOf(expectValue: string): void
|
||||
assertLarger(expectValue: number): void
|
||||
assertLess(expectValue: number): void
|
||||
assertNull(): void
|
||||
assertThrowError(expectValue: string | Function): void
|
||||
assertUndefined(): void
|
||||
assertLargerOrEqual(expectValue: number):void
|
||||
assertLessOrEqual(expectValue: number):void
|
||||
assertNaN():void
|
||||
assertNegUnlimited(): void
|
||||
assertPosUnlimited(): void
|
||||
not(): Assert;
|
||||
assertDeepEquals(expectValue: allExpectType):void
|
||||
assertPromiseIsPending(): Promise<void>
|
||||
assertPromiseIsRejected(): Promise<void>
|
||||
assertPromiseIsRejectedWith(expectValue?: allExpectType): Promise<void>
|
||||
assertPromiseIsRejectedWithError(...expectValue: allExpectType[]): Promise<void>
|
||||
assertPromiseIsResolved(): Promise<void>
|
||||
assertPromiseIsResolvedWith(expectValue?: allExpectType): Promise<void>
|
||||
message(msg: string): Assert
|
||||
}
|
||||
|
||||
export declare function expect(actualValue?: allExpectType): Assert
|
||||
|
||||
export declare class ArgumentMatchers {
|
||||
public static any: allExpectType;
|
||||
public static anyString: string;
|
||||
public static anyBoolean: Boolean;
|
||||
public static anyNumber: Number;
|
||||
public static anyObj: Object;
|
||||
public static anyFunction: Function;
|
||||
public static matchRegexs(regex: RegExp): void
|
||||
}
|
||||
|
||||
declare interface whenResult {
|
||||
afterReturn: (value: allExpectType) => allExpectType
|
||||
afterReturnNothing: () => undefined
|
||||
afterAction: (action: allExpectType) => allExpectType
|
||||
afterThrow: (e_msg: string) => string
|
||||
}
|
||||
|
||||
export declare function when(f:Function): (...args: (allExpectType | void)[]) => whenResult
|
||||
|
||||
export declare interface VerificationMode {
|
||||
times(count: Number): void
|
||||
never(): void
|
||||
once(): void
|
||||
atLeast(count: Number): void
|
||||
atMost(count: Number): void
|
||||
}
|
||||
|
||||
export declare class MockKit {
|
||||
constructor()
|
||||
mockFunc(obj: Object, func: Function): Function
|
||||
mockObject(obj: Object): Object
|
||||
verify(methodName: String, argsArray: Array<allExpectType>): VerificationMode
|
||||
ignoreMock(obj: Object, func: Function): void
|
||||
clear(obj: Object): void
|
||||
clearAll(): void
|
||||
}
|
||||
|
||||
export declare class SysTestKit {
|
||||
static getDescribeName(): string;
|
||||
static getItName(): string;
|
||||
static getItAttribute(): TestType | Size | Level
|
||||
static actionStart(tag: string): void
|
||||
static actionEnd(tag: string): void
|
||||
static existKeyword(keyword: string, timeout?: number): boolean
|
||||
}
|
||||
|
||||
@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Core from './src/main/core';
|
||||
import { DEFAULT, TestType, Size, Level, TAG, PrintTag } from './src/main/Constant';
|
||||
import DataDriver from './src/main/module/config/DataDriver';
|
||||
import ExpectExtend from './src/main/module/assert/ExpectExtend';
|
||||
import OhReport from './src/main/module/report/OhReport';
|
||||
import SysTestKit from './src/main/module/kit/SysTestKit';
|
||||
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect, beforeItSpecified, afterItSpecified, xdescribe, xit } from './src/main/interface';
|
||||
import { MockKit, when } from './src/main/module/mock/MockKit';
|
||||
import ArgumentMatchers from './src/main/module/mock/ArgumentMatchers';
|
||||
import worker from '@ohos.worker';
|
||||
|
||||
class Hypium {
|
||||
static context = new Map();
|
||||
static setData(data) {
|
||||
const core = Core.getInstance();
|
||||
const dataDriver = new DataDriver({ data });
|
||||
core.addService('dataDriver', dataDriver);
|
||||
}
|
||||
|
||||
static setTimeConfig(systemTime) {
|
||||
SysTestKit.systemTime = systemTime;
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
Hypium.context.set(key, value);
|
||||
}
|
||||
|
||||
static get(key) {
|
||||
return Hypium.context.get(key);
|
||||
}
|
||||
|
||||
static hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite) {
|
||||
const core = Core.getInstance();
|
||||
const expectExtend = new ExpectExtend({
|
||||
'id': 'extend'
|
||||
});
|
||||
core.addService('expect', expectExtend);
|
||||
const ohReport = new OhReport({
|
||||
'delegator': abilityDelegator,
|
||||
'abilityDelegatorArguments': abilityDelegatorArguments
|
||||
});
|
||||
SysTestKit.delegator = abilityDelegator;
|
||||
core.addService('report', ohReport);
|
||||
core.init();
|
||||
core.subscribeEvent('spec', ohReport);
|
||||
core.subscribeEvent('suite', ohReport);
|
||||
core.subscribeEvent('task', ohReport);
|
||||
const configService = core.getDefaultService('config');
|
||||
if (abilityDelegatorArguments !== null) {
|
||||
let testParameters = configService.translateParams(abilityDelegatorArguments.parameters);
|
||||
console.info(`${TAG}parameters:${JSON.stringify(testParameters)}`);
|
||||
configService.setConfig(testParameters);
|
||||
}
|
||||
testsuite();
|
||||
core.execute(abilityDelegator);
|
||||
}
|
||||
static async hypiumInitWorkers(abilityDelegator, scriptURL, workerNum = 8, params) {
|
||||
console.info(`${TAG}, hypiumInitWorkers call,${scriptURL}`);
|
||||
let workerPromiseArray = [];
|
||||
|
||||
// 开始统计时间
|
||||
let startTime = await SysTestKit.getRealTime();
|
||||
for (let i = 0; i < workerNum; i++) {
|
||||
// 创建worker线程
|
||||
const workerPromise = Hypium.createWorkerPromise(scriptURL, i, params);
|
||||
workerPromiseArray.push(workerPromise);
|
||||
}
|
||||
const ret = {total: 0, failure: 0, error: 0, pass: 0, ignore: 0, duration: 0};
|
||||
Promise.all(workerPromiseArray).then(async (items) => {
|
||||
console.info(`${TAG}, all result from workers, ${JSON.stringify(items)}`);
|
||||
let allItemList = new Array();
|
||||
// 统计执行结果
|
||||
Hypium.handleWorkerTestResult(ret, allItemList, items);
|
||||
console.info(`${TAG}, all it result, ${JSON.stringify(allItemList)}`);
|
||||
// 统计用例执行结果
|
||||
const retResult = {total: 0, failure: 0, error: 0, pass: 0, ignore: 0, duration: 0};
|
||||
// 标记用例执行结果
|
||||
Hypium.configWorkerItTestResult(retResult, allItemList);
|
||||
// 打印用例结果
|
||||
Hypium.printWorkerTestResult(abilityDelegator, allItemList);
|
||||
// 用例执行完成统计时间
|
||||
let endTime = await SysTestKit.getRealTime();
|
||||
const taskConsuming = endTime - startTime;
|
||||
const message =
|
||||
`\n${PrintTag.OHOS_REPORT_ALL_RESULT}: stream=Test run: runTimes: ${ret.total},total: ${retResult.total}, Failure: ${retResult.failure}, Error: ${retResult.error}, Pass: ${retResult.pass}, Ignore: ${retResult.ignore}` +
|
||||
`\n${PrintTag.OHOS_REPORT_ALL_CODE}: ${retResult.failure > 0 || retResult.error > 0 ? -1 : 0}` +
|
||||
`\n${PrintTag.OHOS_REPORT_ALL_STATUS}: taskconsuming=${taskConsuming > 0 ? taskConsuming : ret.duration}`;
|
||||
abilityDelegator.printSync(message);
|
||||
console.info(`${TAG}, [end] you worker test`);
|
||||
abilityDelegator.finishTest('you worker test finished!!!', 0, () => {});
|
||||
}).catch((e) => {
|
||||
console.info(`${TAG}, [end] error you worker test, ${JSON.stringify(e)}`);
|
||||
abilityDelegator.finishTest('you worker test error finished!!!', 0, () => {});
|
||||
}).finally(() => {
|
||||
console.info(`${TAG}, all promise finally end`);
|
||||
});
|
||||
}
|
||||
// 创建worker线程
|
||||
static createWorkerPromise(scriptURL, i, params) {
|
||||
console.info(`${TAG}, createWorkerPromiser, ${scriptURL}, ${i}`);
|
||||
const workerPromise = new Promise((resolve, reject) => {
|
||||
const workerInstance = new worker.ThreadWorker(scriptURL, {name: `worker_${i}`});
|
||||
console.info(`${TAG}, send data to worker`);
|
||||
// 发送数据到worker线程中
|
||||
workerInstance.postMessage(params);
|
||||
workerInstance.onmessage = function (e) {
|
||||
let currentThreadName = e.data?.currentThreadName;
|
||||
console.info(`${TAG}, receview data from ${currentThreadName}, ${JSON.stringify(e.data)}`);
|
||||
//
|
||||
resolve(e.data?.summary);
|
||||
console.info(`${TAG}, ${currentThreadName} finish`);
|
||||
workerInstance.terminate();
|
||||
};
|
||||
workerInstance.onerror = function (e) {
|
||||
console.info(`${TAG}, worker error, ${JSON.stringify(e)}`);
|
||||
reject(e);
|
||||
workerInstance.terminate();
|
||||
};
|
||||
workerInstance.onmessageerror = function (e) {
|
||||
console.info(`${TAG}, worker message error, ${JSON.stringify(e)}`);
|
||||
reject(e);
|
||||
workerInstance.terminate();
|
||||
};
|
||||
});
|
||||
return workerPromise;
|
||||
}
|
||||
static handleWorkerTestResult(ret, allItemList, items) {
|
||||
console.info(`${TAG}, handleWorkerTestResult, ${JSON.stringify(items)}`);
|
||||
for (const {total, failure, error, pass, ignore, duration, itItemList} of items) {
|
||||
ret.total += total;
|
||||
ret.failure += failure;
|
||||
ret.error += error;
|
||||
ret.pass += pass;
|
||||
ret.ignore += ignore;
|
||||
ret.duration += duration;
|
||||
Hypium.handleItResult(allItemList, itItemList);
|
||||
}
|
||||
}
|
||||
static handleItResult(allItemList, itItemList) {
|
||||
// 遍历所有的用例结果统计最终结果
|
||||
for (const {currentThreadName, description, result} of itItemList) {
|
||||
let item = allItemList.find((it) => it.description === description);
|
||||
if (item) {
|
||||
let itResult = item.result;
|
||||
// 当在worker中出现一次failure就标记为failure, 出现一次error就标记为error, 所有线程都pass才标记为pass
|
||||
if (itResult === 0) {
|
||||
item.result = result;
|
||||
item.currentThreadName = currentThreadName;
|
||||
}
|
||||
} else {
|
||||
let it = {
|
||||
description: description,
|
||||
currentThreadName: currentThreadName,
|
||||
result: result
|
||||
};
|
||||
allItemList.push(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
static configWorkerItTestResult(retResult, allItemList) {
|
||||
console.info(`${TAG}, configWorkerItTestResult, ${JSON.stringify(allItemList)}`);
|
||||
for (const {currentThreadName, description, result} of allItemList) {
|
||||
console.info(`${TAG}, description, ${description}, result,${result}`);
|
||||
retResult.total ++;
|
||||
if (result === 0) {
|
||||
retResult.pass ++;
|
||||
} else if (result === -1) {
|
||||
retResult.error ++;
|
||||
} else if (result === -2) {
|
||||
retResult.failure ++;
|
||||
} else {
|
||||
retResult.ignore ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
static printWorkerTestResult(abilityDelegator, allItemList) {
|
||||
console.info(`${TAG}, printWorkerTestResult, ${JSON.stringify(allItemList)}`);
|
||||
let index = 1;
|
||||
for (const {currentThreadName, description, result} of allItemList) {
|
||||
console.info(`${TAG}, description print, ${description}, result,${result}`);
|
||||
let itArray = description.split('#');
|
||||
let des;
|
||||
let itName;
|
||||
if (itArray.length > 1) {
|
||||
des = itArray[0];
|
||||
itName = itArray[1];
|
||||
} else if (itArray.length > 1) {
|
||||
des = itArray[0];
|
||||
itName = itArray[0];
|
||||
} else {
|
||||
des = 'undefined';
|
||||
itName = 'undefined';
|
||||
}
|
||||
|
||||
let msg = `\n${PrintTag.OHOS_REPORT_WORKER_STATUS}: class=${des}`;
|
||||
msg += `\n${PrintTag.OHOS_REPORT_WORKER_STATUS}: test=${itName}`;
|
||||
msg += `\n${PrintTag.OHOS_REPORT_WORKER_STATUS}: current=${index}`;
|
||||
msg += `\n${PrintTag.OHOS_REPORT_WORKER_STATUS}: CODE=${result}`;
|
||||
abilityDelegator.printSync(msg);
|
||||
index ++;
|
||||
}
|
||||
}
|
||||
static hypiumWorkerTest(abilityDelegator, abilityDelegatorArguments, testsuite, workerPort) {
|
||||
console.info(`${TAG}, hypiumWorkerTest call`);
|
||||
SysTestKit.workerPort = workerPort;
|
||||
let currentWorkerName = workerPort.name;
|
||||
console.info(`${TAG}, hypiumWorkerTest_currentWorkerName: ${currentWorkerName}`);
|
||||
Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite);
|
||||
|
||||
}
|
||||
|
||||
static registerAssert(customAssertion) {
|
||||
const core = Core.getInstance();
|
||||
const expectService = core.getDefaultService('expect');
|
||||
let matchers = {};
|
||||
matchers[customAssertion.name] = customAssertion;
|
||||
expectService.addMatchers(matchers);
|
||||
expectService.customMatchers.push(customAssertion.name);
|
||||
console.info(`${TAG}success to register the ${customAssertion.name}`);
|
||||
}
|
||||
|
||||
static unregisterAssert(customAssertion) {
|
||||
const core = Core.getInstance();
|
||||
const expectService = core.getDefaultService('expect');
|
||||
let customAssertionName = typeof customAssertion === 'function' ? customAssertion.name : customAssertion;
|
||||
expectService.removeMatchers(customAssertionName);
|
||||
console.info(`${TAG}success to unregister the ${customAssertionName}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export {
|
||||
Hypium,
|
||||
Core,
|
||||
DEFAULT,
|
||||
TestType,
|
||||
Size,
|
||||
Level,
|
||||
DataDriver,
|
||||
ExpectExtend,
|
||||
OhReport,
|
||||
SysTestKit,
|
||||
describe, beforeAll, beforeEach, afterEach, afterAll, it, expect, beforeItSpecified, afterItSpecified, xdescribe, xit,
|
||||
MockKit, when,
|
||||
ArgumentMatchers
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestType, Size, Level } from "./src/main/Constant";
|
||||
|
||||
export declare function xdescribe(testSuiteName: string, func: Function): void;
|
||||
|
||||
export declare namespace xdescribe {
|
||||
function reason(reason: string): any;
|
||||
};
|
||||
|
||||
export declare function describe(testSuiteName: string, func: Function): void;
|
||||
|
||||
export declare function xit(testCaseName: string, attribute: TestType | Size | Level, func: Function): void;
|
||||
|
||||
export declare namespace xit {
|
||||
function reason(reason: string): any;
|
||||
};
|
||||
|
||||
export declare function it(testCaseName: string, attribute: TestType | Size | Level, func: Function): void;
|
||||
@ -0,0 +1 @@
|
||||
{"name":"@ohos/hypium","version":"1.0.24","description":"A unit test framework for OpenHarmony application","main":"index.js","keywords":["测试框架","except","mock"],"author":"huawei","license":"Apache-2.0","repository":"https://gitee.com/openharmony/testfwk_arkxtest","homepage":"https://gitee.com/openharmony/testfwk_arkxtest","dependencies":{},"metadata":{"sourceRoots":["./src/main"],"debug":true,"useNormalizedOHMUrl":false},"compatibleSdkVersion":17,"compatibleSdkType":"HarmonyOS","obfuscated":false}
|
||||
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* define the testcase type : TestType, Size , Level
|
||||
*/
|
||||
export const TAG = '[Hypium]';
|
||||
|
||||
export const DEFAULT = 0B0000;
|
||||
|
||||
export class PrintTag {
|
||||
static OHOS_REPORT_WORKER_STATUS = 'OHOS_REPORT_WORKER_STATUS';
|
||||
static OHOS_REPORT_ALL_RESULT = 'OHOS_REPORT_ALL_RESULT';
|
||||
static OHOS_REPORT_ALL_CODE = 'OHOS_REPORT_ALL_CODE';
|
||||
static OHOS_REPORT_ALL_STATUS = 'OHOS_REPORT_ALL_STATUS';
|
||||
static OHOS_REPORT_RESULT = 'OHOS_REPORT_RESULT';
|
||||
static OHOS_REPORT_CODE = 'OHOS_REPORT_CODE';
|
||||
static OHOS_REPORT_STATUS = 'OHOS_REPORT_STATUS';
|
||||
static OHOS_REPORT_SUM = 'OHOS_REPORT_SUM';
|
||||
static OHOS_REPORT_STATUS_CODE = 'OHOS_REPORT_STATUS_CODE';
|
||||
};
|
||||
|
||||
export class TestType {
|
||||
static FUNCTION = 0B1;
|
||||
static PERFORMANCE = 0B1 << 1;
|
||||
static POWER = 0B1 << 2;
|
||||
static RELIABILITY = 0B1 << 3;
|
||||
static SECURITY = 0B1 << 4;
|
||||
static GLOBAL = 0B1 << 5;
|
||||
static COMPATIBILITY = 0B1 << 6;
|
||||
static USER = 0B1 << 7;
|
||||
static STANDARD = 0B1 << 8;
|
||||
static SAFETY = 0B1 << 9;
|
||||
static RESILIENCE = 0B1 << 10;
|
||||
};
|
||||
|
||||
export class Size {
|
||||
static SMALLTEST = 0B1 << 16;
|
||||
static MEDIUMTEST = 0B1 << 17;
|
||||
static LARGETEST = 0B1 << 18;
|
||||
};
|
||||
|
||||
export class Level {
|
||||
static LEVEL0 = 0B1 << 24;
|
||||
static LEVEL1 = 0B1 << 25;
|
||||
static LEVEL2 = 0B1 << 26;
|
||||
static LEVEL3 = 0B1 << 27;
|
||||
static LEVEL4 = 0B1 << 28;
|
||||
};
|
||||
|
||||
export const TESTTYPE = {
|
||||
'function': 1,
|
||||
'performance': 1 << 1,
|
||||
'power': 1 << 2,
|
||||
'reliability': 1 << 3,
|
||||
'security': 1 << 4,
|
||||
'global': 1 << 5,
|
||||
'compatibility': 1 << 6,
|
||||
'user': 1 << 7,
|
||||
'standard': 1 << 8,
|
||||
'safety': 1 << 9,
|
||||
'resilience': 1 << 10,
|
||||
};
|
||||
|
||||
export const LEVEL = {
|
||||
'0': 1 << 24,
|
||||
'1': 1 << 25,
|
||||
'2': 1 << 26,
|
||||
'3': 1 << 27,
|
||||
'4': 1 << 28,
|
||||
};
|
||||
|
||||
export const SIZE = {
|
||||
'small': 1 << 16,
|
||||
'medium': 1 << 17,
|
||||
'large': 1 << 18,
|
||||
};
|
||||
|
||||
export const KEYSET = [
|
||||
'-s class', '-s notClass', '-s suite', '-s itName',
|
||||
'-s level', '-s testType', '-s size', '-s timeout',
|
||||
'-s dryRun', '-s random', '-s breakOnError', '-s stress',
|
||||
'-s coverage', '-s skipMessage', '-s runSkipped',
|
||||
'class', 'notClass', 'suite', 'itName',
|
||||
'level', 'testType', 'size', 'timeout', 'dryRun', 'random',
|
||||
'breakOnError', 'stress', 'coverage', 'skipMessage', 'runSkipped'
|
||||
];
|
||||
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {SuiteService, SpecService, ExpectService, ReportService} from './service';
|
||||
import {ConfigService} from './module/config/configService';
|
||||
import {SpecEvent, TaskEvent, SuiteEvent} from './event';
|
||||
|
||||
/**
|
||||
* core service for execute testcase.
|
||||
*/
|
||||
class Core {
|
||||
static getInstance() {
|
||||
if (!this.instance) {
|
||||
this.instance = new Core();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.instance = null;
|
||||
this.services = {
|
||||
suite: {},
|
||||
spec: {},
|
||||
config: {},
|
||||
expect: {},
|
||||
log: {},
|
||||
report: {}
|
||||
|
||||
};
|
||||
this.events = {
|
||||
suite: {},
|
||||
spec: {},
|
||||
task: {}
|
||||
};
|
||||
}
|
||||
|
||||
addService(name, service) {
|
||||
let serviceObj = {};
|
||||
if (!this.services[name]) {
|
||||
this.services[name] = serviceObj;
|
||||
} else {
|
||||
serviceObj = this.services[name];
|
||||
}
|
||||
serviceObj[service.id] = service;
|
||||
}
|
||||
|
||||
getDefaultService(name) {
|
||||
return this.services[name].default;
|
||||
}
|
||||
|
||||
getServices(name) {
|
||||
return this.services[name];
|
||||
}
|
||||
|
||||
registerEvent(serviceName, event) {
|
||||
let eventObj = {};
|
||||
if (!this.events[serviceName]) {
|
||||
this.events[serviceName] = eventObj;
|
||||
} else {
|
||||
eventObj = this.events[serviceName];
|
||||
}
|
||||
eventObj[event.id] = event;
|
||||
}
|
||||
|
||||
unRegisterEvent(serviceName, eventID) {
|
||||
const eventObj = this.events[serviceName];
|
||||
if (eventObj) {
|
||||
delete eventObj[eventID];
|
||||
}
|
||||
}
|
||||
|
||||
subscribeEvent(serviceName, serviceObj) {
|
||||
const eventObj = this.events[serviceName];
|
||||
if (eventObj) {
|
||||
for (const attr in eventObj) {
|
||||
eventObj[attr]['subscribeEvent'](serviceObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fireEvents(serviceName, eventName) {
|
||||
const eventObj = this.events[serviceName];
|
||||
if (!eventObj) {
|
||||
return;
|
||||
}
|
||||
for (const attr in eventObj) {
|
||||
await eventObj[attr][eventName]();
|
||||
}
|
||||
}
|
||||
|
||||
addToGlobal(apis) {
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
for (let api in apis) {
|
||||
globalThis[api] = apis[api];
|
||||
}
|
||||
}
|
||||
for (const api in apis) {
|
||||
this[api] = apis[api];
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.addService('suite', new SuiteService({id: 'default'}));
|
||||
this.addService('spec', new SpecService({id: 'default'}));
|
||||
this.addService('expect', new ExpectService({id: 'default'}));
|
||||
this.addService('report', new ReportService({id: 'default'}));
|
||||
this.addService('config', new ConfigService({id: 'default'}));
|
||||
this.registerEvent('task', new TaskEvent({id: 'default', coreContext: this}));
|
||||
this.registerEvent('suite', new SuiteEvent({id: 'default', coreContext: this}));
|
||||
this.registerEvent('spec', new SpecEvent({id: 'default', coreContext: this}));
|
||||
this.subscribeEvent('spec', this.getDefaultService('report'));
|
||||
this.subscribeEvent('suite', this.getDefaultService('report'));
|
||||
this.subscribeEvent('task', this.getDefaultService('report'));
|
||||
const context = this;
|
||||
for (const key in this.services) {
|
||||
const serviceObj = this.services[key];
|
||||
for (const serviceID in serviceObj) {
|
||||
const service = serviceObj[serviceID];
|
||||
service.init(context);
|
||||
|
||||
if (typeof service.apis !== 'function') {
|
||||
continue;
|
||||
}
|
||||
const apis = service.apis();
|
||||
if (apis) {
|
||||
this.addToGlobal(apis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execute(abilityDelegator) {
|
||||
const suiteService = this.getDefaultService('suite');
|
||||
const configService = this.getDefaultService('config');
|
||||
if (configService['dryRun'] === 'true') {
|
||||
(async function () {
|
||||
await suiteService.dryRun(abilityDelegator);
|
||||
})();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
suiteService.execute();
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
export default Core;
|
||||
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class SpecEvent {
|
||||
constructor(attr) {
|
||||
this.id = attr.id;
|
||||
this.coreContext = attr.context;
|
||||
this.eventMonitors = [];
|
||||
}
|
||||
|
||||
subscribeEvent(service) {
|
||||
this.eventMonitors.push(service);
|
||||
}
|
||||
|
||||
async specStart() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
await monitor['specStart']();
|
||||
}
|
||||
}
|
||||
|
||||
async specDone() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
await monitor['specDone']();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SuiteEvent {
|
||||
constructor(attr) {
|
||||
this.id = attr.id;
|
||||
this.suiteContext = attr.coreContext;
|
||||
this.eventMonitors = [];
|
||||
}
|
||||
|
||||
subscribeEvent(service) {
|
||||
this.eventMonitors.push(service);
|
||||
}
|
||||
|
||||
async suiteStart() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
await monitor['suiteStart']();
|
||||
}
|
||||
}
|
||||
|
||||
async suiteDone() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
await monitor['suiteDone']();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TaskEvent {
|
||||
constructor(attr) {
|
||||
this.id = attr.id;
|
||||
this.coreContext = attr.coreContext;
|
||||
this.eventMonitors = [];
|
||||
}
|
||||
|
||||
subscribeEvent(service) {
|
||||
this.eventMonitors.push(service);
|
||||
}
|
||||
|
||||
async taskStart() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
await monitor['taskStart']();
|
||||
}
|
||||
}
|
||||
|
||||
async taskDone() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
await monitor['taskDone']();
|
||||
}
|
||||
}
|
||||
|
||||
incorrectFormat() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
monitor['incorrectFormat']();
|
||||
}
|
||||
}
|
||||
|
||||
incorrectTestSuiteFormat() {
|
||||
for (const monitor of this.eventMonitors) {
|
||||
monitor.incorrectTestSuiteFormat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SpecEvent, TaskEvent, SuiteEvent };
|
||||
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Core from './core';
|
||||
|
||||
const core = Core.getInstance();
|
||||
|
||||
const describe = function (desc, func) {
|
||||
return Reflect.has(core, 'describe') ? core.describe(desc, func) : (desc, func) => { };
|
||||
};
|
||||
const it = function (desc, filter, func) {
|
||||
return Reflect.has(core, 'it') ? core.it(desc, filter, func) : (desc, filter, func) => { };
|
||||
};
|
||||
const beforeItSpecified = function (itDescs, func) {
|
||||
return Reflect.has(core, 'beforeItSpecified') ? core.beforeItSpecified(itDescs, func) : (itDescs, func) => { };
|
||||
};
|
||||
|
||||
const afterItSpecified = function (itDescs, func) {
|
||||
return Reflect.has(core, 'afterItSpecified') ? core.afterItSpecified(itDescs, func) : (itDescs, func) => { };
|
||||
};
|
||||
const beforeEach = function (func) {
|
||||
return Reflect.has(core, 'beforeEach') ? core.beforeEach(func) : (func) => { };
|
||||
};
|
||||
const afterEach = function (func) {
|
||||
return Reflect.has(core, 'afterEach') ? core.afterEach(func) : (func) => { };
|
||||
};
|
||||
const beforeAll = function (func) {
|
||||
return Reflect.has(core, 'beforeAll') ? core.beforeAll(func) : (func) => { };
|
||||
};
|
||||
const afterAll = function (func) {
|
||||
return Reflect.has(core, 'afterAll') ? core.afterAll(func) : (func) => { };
|
||||
};
|
||||
const expect = function (actualValue) {
|
||||
return Reflect.has(core, 'expect') ? core.expect(actualValue) : (actualValue) => { };
|
||||
};
|
||||
|
||||
const xdescribe = function (desc, func) {
|
||||
return Reflect.has(core, 'xdescribe') ? core.xdescribe(desc, func, null) : (desc, func, reason) => { };
|
||||
};
|
||||
xdescribe.reason = (reason) => {
|
||||
return (desc, func) => {
|
||||
return Reflect.has(core, 'xdescribe') ? core.xdescribe(desc, func, reason) : (desc, func, reason) => { };
|
||||
};
|
||||
};
|
||||
const xit = function (desc, filter, func) {
|
||||
return Reflect.has(core, 'xit') ? core.xit(desc, filter, func, null) : (desc, filter, func, reason) => { };
|
||||
};
|
||||
xit.reason = (reason) => {
|
||||
return (desc, filter, func) => {
|
||||
return Reflect.has(core, 'xit') ? core.xit(desc, filter, func, reason) : (desc, filter, func, reason) => { };
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
describe, it, beforeAll, beforeEach, afterEach, afterAll, expect, beforeItSpecified, afterItSpecified, xdescribe, xit
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.hypium.myapplication",
|
||||
"debug": true,
|
||||
"versionCode": 1000000,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 50005017,
|
||||
"targetAPIVersion": 50005017,
|
||||
"apiReleaseType": "Release",
|
||||
"compileSdkVersion": "5.0.5.165",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "hypium",
|
||||
"type": "har",
|
||||
"deviceTypes": [
|
||||
"default",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/hypium",
|
||||
"installationFree": false,
|
||||
"virtualMachine": "ark12.0.6.0",
|
||||
"compileMode": "esmodule",
|
||||
"dependencies": []
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2024 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import assertNull from './assertNull';
|
||||
import assertClose from './assertClose';
|
||||
import assertContain from './assertContain';
|
||||
import assertLess from './assertLess';
|
||||
import assertLarger from './assertLarger';
|
||||
import assertFail from './assertFail';
|
||||
import assertUndefined from './assertUndefined';
|
||||
import assertFalse from './assertFalse';
|
||||
import assertInstanceOf from './assertInstanceOf';
|
||||
import assertThrowError from './assertThrowError';
|
||||
import assertLargerOrEqual from './assertLargerOrEqual';
|
||||
import assertLessOrEqual from './assertLessOrEqual';
|
||||
import assertNaN from './assertNaN';
|
||||
import assertNegUnlimited from './assertNegUnlimited';
|
||||
import assertPosUnlimited from './assertPosUnlimited';
|
||||
import assertDeepEquals from './deepEquals/assertDeepEquals';
|
||||
import assertPromiseIsPending from './assertPromiseIsPending';
|
||||
import assertPromiseIsRejected from './assertPromiseIsRejected';
|
||||
import assertPromiseIsRejectedWith from './assertPromiseIsRejectedWith';
|
||||
import assertPromiseIsRejectedWithError from './assertPromiseIsRejectedWithError';
|
||||
import assertPromiseIsResolved from './assertPromiseIsResolved';
|
||||
import assertPromiseIsResolvedWith from './assertPromiseIsResolvedWith';
|
||||
class ExpectExtend {
|
||||
constructor(attr) {
|
||||
this.id = attr.id;
|
||||
this.matchers = {};
|
||||
}
|
||||
|
||||
extendsMatchers() {
|
||||
this.matchers.assertNull = assertNull;
|
||||
this.matchers.assertClose = assertClose;
|
||||
this.matchers.assertContain = assertContain;
|
||||
this.matchers.assertLess = assertLess;
|
||||
this.matchers.assertLarger = assertLarger;
|
||||
this.matchers.assertFail = assertFail;
|
||||
this.matchers.assertUndefined = assertUndefined;
|
||||
this.matchers.assertFalse = assertFalse;
|
||||
this.matchers.assertInstanceOf = assertInstanceOf;
|
||||
this.matchers.assertThrowError = assertThrowError;
|
||||
this.matchers.assertLargerOrEqual = assertLargerOrEqual;
|
||||
this.matchers.assertLessOrEqual = assertLessOrEqual;
|
||||
this.matchers.assertNaN = assertNaN;
|
||||
this.matchers.assertNegUnlimited = assertNegUnlimited;
|
||||
this.matchers.assertPosUnlimited = assertPosUnlimited;
|
||||
this.matchers.assertDeepEquals = assertDeepEquals;
|
||||
this.matchers.assertPromiseIsPending = assertPromiseIsPending;
|
||||
this.matchers.assertPromiseIsRejected = assertPromiseIsRejected;
|
||||
this.matchers.assertPromiseIsRejectedWith = assertPromiseIsRejectedWith;
|
||||
this.matchers.assertPromiseIsRejectedWithError = assertPromiseIsRejectedWithError;
|
||||
this.matchers.assertPromiseIsResolved = assertPromiseIsResolved;
|
||||
this.matchers.assertPromiseIsResolvedWith = assertPromiseIsResolvedWith;
|
||||
}
|
||||
|
||||
init(coreContext) {
|
||||
this.coreContext = coreContext;
|
||||
this.extendsMatchers();
|
||||
const expectService = this.coreContext.getDefaultService('expect');
|
||||
expectService.addMatchers(this.matchers);
|
||||
}
|
||||
|
||||
apis() {
|
||||
return {
|
||||
'expect': function (actualValue) {
|
||||
return this.coreContext.getDefaultService('expect').expect(actualValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ExpectExtend;
|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function assertClose(actualValue, expected) {
|
||||
if (actualValue === null && expected[0] === null) {
|
||||
throw new Error('actualValue and expected can not be both null!!!');
|
||||
}
|
||||
let result;
|
||||
let diff = Math.abs(expected[0] - actualValue);
|
||||
let actualAbs = Math.abs(actualValue);
|
||||
if ((actualAbs - 0) === 0) {
|
||||
if ((diff - 0) === 0) {
|
||||
result = true;
|
||||
} else {
|
||||
result = false;
|
||||
}
|
||||
} else if (diff / actualAbs < expected[1]) {
|
||||
result = true;
|
||||
} else {
|
||||
result = false;
|
||||
}
|
||||
return {
|
||||
pass: result,
|
||||
message: '|' + actualValue + ' - ' + expected[0] + '|/' + actualValue + ' is not less than ' + expected[1]
|
||||
};
|
||||
}
|
||||
|
||||
export default assertClose;
|
||||
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function assertContain(actualValue, expect) {
|
||||
let result = false;
|
||||
if (Object.prototype.toString.call(actualValue).indexOf('Array')) {
|
||||
for (let i in actualValue) {
|
||||
if (actualValue[i] == expect[0]) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let type = Object.prototype.toString.call(actualValue);
|
||||
if (type === '[object String]') {
|
||||
result = actualValue.indexOf(expect[0]) >= 0;
|
||||
}
|
||||
return {
|
||||
pass: result,
|
||||
message: 'expect false, ' + actualValue + ' do not have ' + expect[0]
|
||||
};
|
||||
}
|
||||
|
||||
export default assertContain;
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function assertFail() {
|
||||
return {
|
||||
pass: false,
|
||||
message: 'fail '
|
||||
};
|
||||
}
|
||||
|
||||
export default assertFail;
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function assertFalse(actualValue) {
|
||||
return {
|
||||
pass: (actualValue) === false,
|
||||
message: 'expect false, actualValue is ' + actualValue
|
||||
};
|
||||
}
|
||||
|
||||
export default assertFalse;
|
||||
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2022 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function assertInstanceOf(actualValue, expected) {
|
||||
if (Object.prototype.toString.call(actualValue) == '[object ' + expected[0] + ']') {
|
||||
return {
|
||||
pass: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
pass: false,
|
||||
message: actualValue + ' is ' + Object.prototype.toString.call(actualValue) + 'not ' + expected[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default assertInstanceOf;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user