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:
root
2026-05-01 16:05:22 +08:00
parent 5e4fb8222a
commit e779c16595
173 changed files with 9954 additions and 1792 deletions

View File

@ -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"
]
}
]
}
]
}

View File

@ -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
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/common

6
common/hvigorfile.ts Normal file
View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View File

@ -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);
}
}
}

View File

@ -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;

View 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;
}
}

View File

@ -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);
}
}

View File

@ -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)
];
// ========= 工具名称映射 =========

View File

@ -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;
}
}

View 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;

View File

@ -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
]
}
}
}

View File

@ -1,8 +0,0 @@
{
"apiType": "stageMode",
"targets": [
{
"name": "default"
}
]
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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');
}
}

View File

@ -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"
}
]
}
}

View File

@ -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

View File

@ -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

View File

@ -1,5 +0,0 @@
{
"src": [
"pages/Index"
]
}

View 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;
}

View File

@ -1,5 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"

1
features/chat/chat Symbolic link
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/features/chat

View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View File

@ -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"
}
}

View File

@ -0,0 +1 @@
../../../../common

View 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)')
}
}

View File

@ -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)')
}
}
}

View File

@ -1,11 +1,10 @@
{
"module": {
"name": "librcon",
"name": "chat",
"type": "har",
"requestPermissions": [
],
"description": "TrulyMEM chat feature module",
"deviceTypes": [
"default",
"phone",
"tablet",
"2in1"
]

View File

@ -1 +0,0 @@
// Common business module - placeholder for shared business logic

View File

@ -1,8 +0,0 @@
{
"apiType": "stageMode",
"targets": [
{
"name": "default"
}
]
}

View File

@ -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"
}
}

View 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;
}

View File

@ -1,5 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"

1
features/graph/graph Symbolic link
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/features/graph

View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View 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"
}
}
}

View File

@ -0,0 +1 @@
../../../../common

View 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;
}

View File

@ -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%')
}
}
}

View File

@ -0,0 +1,12 @@
{
"module": {
"name": "graph",
"type": "har",
"description": "TrulyMEM graph feature module",
"deviceTypes": [
"phone",
"tablet",
"2in1"
]
}
}

View 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;
}

View File

@ -1,5 +1,7 @@
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"

View File

@ -0,0 +1,6 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: []
};

View 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"
}
}
}

View File

@ -0,0 +1 @@
../../../../common

1
features/settings/settings Symbolic link
View File

@ -0,0 +1 @@
/home/program/TrulyMEM-TrueHumanMEM/features/settings

View 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;
}
}

View File

@ -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%')

View 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
View 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
View 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"
}
}
}

View File

@ -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"

View File

@ -0,0 +1,4 @@
## 1.0.0
- 修复once断言问题
## 1.0.0-rc
- 提供DevEco Studio预览器场景使能的MockSetup装饰器

View File

@ -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

View File

@ -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) ,请自由地享受和参与开源。

View File

@ -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"
}
]
}

View File

@ -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';

View File

@ -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';

View 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;

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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'
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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
};

View File

@ -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;

View File

@ -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;

View File

@ -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"
]
}
}

View File

@ -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"
}
}
}

View 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.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;
}

View File

@ -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
代码告警整改

View File

@ -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

View File

@ -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。

View File

@ -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"
}
]
}

View 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;
}

View File

@ -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
}

View File

@ -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
};

View File

@ -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;

View File

@ -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}

View File

@ -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'
];

View File

@ -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;

View File

@ -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 };

View File

@ -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
};

View File

@ -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": []
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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