14 Commits

Author SHA1 Message Date
150e2d5607 docs: 修正文档与代码实现的差异
以代码为准,修正以下内容:
1. Actions 表格添加 task_link_info 操作
2. recall 参数补充 depth 字段
3. 导入路径更新:waterflow -> waterflow-ts/dist/...
4. installTrulyMEM 需要 await(返回 Promise)
5. 新增 API 名称映射章节(mapToolIdToApiName/mapApiNameToToolId)
6. 新增 task_link_info 使用示例
2026-04-17 14:07:04 +08:00
38941ff2b5 fix: 修复 sql.js Uint8Array 与 fs.writeFile 的兼容性问题
sql.js export() 返回 Uint8Array,需用 Buffer.from() 转换后再写入文件
解决潜在的类型不匹配错误
2026-04-17 13:47:14 +08:00
53d6940994 fix: 修复 AI 工具调用的两个问题
问题1: AI 生成的参数格式与工具期望不一致
- 改进工具 description,添加 commit 操作的三元组格式说明和示例
- 完善 input_schema,详细描述 triplets 的 subject/relation/object 字段
- 在 description 中明确说明必填字段

问题2: 工具名称格式不符合 API 要求
- OpenAI/DeepSeek API 要求工具名称符合 ^[a-zA-Z0-9_-]+$
- 内部 ID "builtin:graph_memory" 含冒号,不符合要求
- 添加 apiName 属性提供 API 兼容名称 "graph_memory"
- 添加 mapToolIdToApiName/mapApiNameToToolId 映射函数

同时修正 import 路径:waterflow/... -> waterflow-ts/dist/...
2026-04-17 13:08:18 +08:00
16327dec72 docs: 更新README - 修正Skill名称并添加Skill定义格式说明
- 修正Skill列表中的名称:persona/task(WaterFlow使用目录名)
- 添加Skill定义格式说明,解释WaterFlow如何解析SKILL.md
- 说明name/description/allowed-tools等字段的提取规则
- 中英文版本同步更新
2026-04-16 17:25:24 +08:00
9e2f390332 fix: 修复Skill定义兼容性 - 整合description和when_to_use到Markdown body
移除frontmatter中不被WaterFlow处理的冗余字段:
- name: WaterFlow使用目录名作为skill名称
- description: WaterFlow从Markdown #标题提取
- when_to_use: 不被处理,整合到body中

修改后的SKILL.md完全兼容WaterFlow SkillLoader:
- description从#标题正确提取
- allowed-tools正确转换为allowedTools
- arguments正确映射
- user-invocable正确转换为userInvocable
2026-04-16 17:22:56 +08:00
56e787a5a8 fix: 修正 WaterFlow Skill/Tool 兼容性问题 + 类型声明完善
Skill 格式修正:
- allowed_tools → allowed-tools (kebab-case)
- user_invocable → user-invocable (kebab-case)
- persona skill name: graph_memory_persona → persona (匹配目录名)
- task skill name: graph_memory_task → task (匹配目录名)

Tool 接口优化:
- installTrulyMEM: require() → ESM dynamic import
- handler: 添加 abort 信号检查
- graph_database: _platform 类型 any → Platform
- graph_database: 修复 fs null 检查和 ArrayBuffer 类型转换

类型声明完善:
- waterflow.d.ts 从 193 行扩展到 400+ 行
- 添加 SchemaType, ToolFeatures, ToolMetadata 等完整类型
- 添加 network, tools, builtinTools 等缺失字段
- 添加 waterflow/runtime/core/tools/builtin 模块声明
- 添加 waterflow/runtime/core/tools/tool_registry 模块声明

清理:
- 删除 TRACKING.md, 重构.md, migration_plan.md, todo_progress.md
- 删除 docs/integration/waterflow-design.md
2026-04-16 15:16:55 +08:00
13e3c662ac refactor: adapt to WaterFlow framework - zero source changes required
- Remove duplicate platform layer and tool interface definitions
- Add BFS breadth-first search with depth annotation (sync from main)
- Use WaterFlow platform.fs for binary storage instead of custom storage
- Add installTrulyMEM() one-line registration function
- Update SKILL.md with 10 complete operations
- Add waterflow.d.ts type declarations
- Update bilingual README with simplified installation guide
- Build passes with 0 errors
2026-04-16 11:48:58 +08:00
e8c275c8f8 docs: expand usage instructions with detailed steps 2026-04-15 16:15:35 +08:00
931617624e chore: add TypeScript build artifacts to gitignore 2026-04-15 16:11:10 +08:00
ee2fd18fec docs: add separate bilingual README files with cross-links 2026-04-15 16:09:15 +08:00
aa18c1c8b1 docs: add bilingual README with WaterFlow usage instructions 2026-04-15 15:51:54 +08:00
d05bb5507f restore: add README and pic files
- Add README.md with WaterFlow usage instructions
- Restore pic/ folder with icons
2026-04-15 15:49:09 +08:00
904661d73f feat: migrate to TypeScript for WaterFlow framework
- Add TypeScript graph memory module (GraphDatabase, MemoryService)
- Add GraphMemoryTool for WaterFlow Tool interface
- Add bundled-skills for graph_memory, persona, task
- Remove Python code (core/, ui/, tests/, etc.)
- Remove redundant docs and build files
- Keep only ts/, docs/integration/, .gitignore, LICENSE
2026-04-15 15:45:12 +08:00
43172e257a docs: add TrulyMEM → WaterFlow migration design document
- TypeScript reimplementation of graph memory system
- Include: GraphDatabase, MemoryService, GraphMemoryTool
- Directory structure and implementation patterns
- Test plan with 50+ test cases
2026-04-15 14:25:51 +08:00
199 changed files with 5728 additions and 15104 deletions

81
.gitignore vendored
View File

@ -1,19 +1,72 @@
# Build cache # Python
.hvigor/
entry/build/default/
trulymem-core/build/default/
trulymem-core/.preview/
build/
dist/
# Python cache
__pycache__/ __pycache__/
*.pyc *.py[cod]
*$py.class
*.so
.Python
develop-eggs/
dist/
downloads/
eggs/
.lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Database # Virtual Environment
graph_memory.db venv/
test_venv/
ENV/
env/
# IDE # IDE
.idea/
.vscode/ .vscode/
*.iml .idea/
.arts/
.codeartsdoer/
*.swp
*.swo
*~
# Database
*.db
*.sqlite
*.sqlite3
# Logs
*.log
logs/
# OS
.DS_Store
Thumbs.db
# Sensitive
.env
*.key
*.pem
config.json
# Build
dist/
# Temporary
*.tmp
*.bak
# AI Generated
jimeng*.png
# Test Cache
.pytest_cache/
# TypeScript
node_modules/
dist/
*.tsbuildinfo
tsconfig.tsbuildinfo

View File

@ -1,10 +0,0 @@
{
"app": {
"bundleName": "com.trulymem.app",
"vendor": "trulymem",
"versionCode": 1000001,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}

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

196
LICENSE Normal file
View File

@ -0,0 +1,196 @@
SPDX-License-Identifier: GPL-3.0-or-later
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2026 jianf
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
================================================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2026 jianf
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of the program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its author. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers. In our view, they should not
allow patents to restrict development and use of software on
general-purpose computers. But in those that do, we wish to avoid the
special danger that patents applied to a free program could make it
effectively proprietary. To prevent this, the GPL assures that patents
cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or menu items, or similar,
each item in the list is treated as if it were an independent command
or menu item. If the interface presents a list of options as a dialog
box, the list is treated as a single option.
[The full text of the GPL v3 license continues with sections 1-17,
but is truncated here for brevity. The complete license text is
available at https://www.gnu.org/licenses/gpl-3.0.txt]
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) 2026 jianf
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
TrulyMEM Copyright (C) 2026 jianf
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

249
README.md Normal file
View File

@ -0,0 +1,249 @@
# TrulyMEM - WaterFlow 适配版
让 AI 拥有真正的长期记忆能力 - WaterFlow 框架适配版
[English Version](./README_EN.md)
---
## 简介
本项目是将 TrulyMEM 的图记忆能力迁移到 WaterFlow 框架的 TypeScript 实现。
作为 WaterFlow 的内置模块,提供图记忆功能:
- **recall**: 检索记忆
- **commit**: 写入记忆
- **purge**: 删除记忆
- **introspect**: 查看状态
- **persona_update/clear**: 人设管理
- **task_create/set_state/delete**: 任务管理
---
## 目录结构
```
ts/
├── src/runtime/core/
│ ├── graph_memory/ # 图记忆核心模块
│ │ ├── types.ts # 类型定义
│ │ ├── graph_database.ts # 图数据库
│ │ ├── memory_service.ts # 记忆服务
│ │ └── index.ts # 模块导出
│ └── tools/
│ └── builtin/
│ └── graph_memory_tool.ts # Tool 实现
├── bundled-skills/ # Skill 定义
│ └── graph_memory/
│ ├── SKILL.md # 记忆操作
│ ├── persona/SKILL.md # 人设管理
│ └── task/SKILL.md # 任务管理
├── package.json # 项目配置
└── tsconfig.json # TypeScript 配置
```
---
## 在 WaterFlow 中使用
本模块完全不动 WaterFlow 源码,只需在你的入口文件中注册即可。
### 快速开始(推荐)
#### 步骤 1安装依赖
```bash
npm install /path/to/TrulyMEM-TrueHumanMEM/ts
```
或在 `package.json` 中添加:
```json
{
"dependencies": {
"trulymem-waterflow": "file:../TrulyMEM-TrueHumanMEM/ts"
}
}
```
然后运行:
```bash
npm install
```
#### 步骤 2在你的入口文件中注册
只需两行代码,完全不动 WaterFlow 源码:
```typescript
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { installTrulyMEM } from 'trulymem/tools';
// 一行安装,返回配置好的 ToolRegistry
const registry = await installTrulyMEM(getPlatform(), 'my-session-id');
// 继续组装 WaterFlow...
const toolExecutor = new ToolExecutor(registry);
```
### 手动注册(更灵活)
如果你想自己控制 ToolRegistry 的创建:
```typescript
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { initializeToolRegistry } from 'waterflow-ts/dist/runtime/core/tools/builtin/index.js';
import { registerGraphMemoryTool } from 'trulymem/tools';
const platform = getPlatform();
const registry = initializeToolRegistry(platform);
// 注册图记忆工具
registerGraphMemoryTool(registry, 'my-session-id');
// 继续组装...
```
### 使用 SkillAI Agent 调用)
#### 步骤 1配置 Skill 来源
```typescript
const config = {
...DEFAULT_SKILL_LOADER_CONFIG,
sources: {
...DEFAULT_SKILL_LOADER_CONFIG.sources,
bundled: './node_modules/trulymem-waterflow/bundled-skills'
},
enabledSources: ['project', 'bundled']
};
```
#### 步骤 2通过 Agent 调用
AI Agent 会自动读取 SKILL.md 并调用 `builtin:graph_memory` 工具。
#### 可用 Skill 列表
| Skill 名称 | 功能 | 使用场景 |
|------------|------|----------|
| `graph_memory` | 记忆 CRUD | 读取/写入/删除记忆 |
| `persona` | 人设管理 | 设置 AI 角色性格 |
| `task` | 任务管理 | 创建/更新长期任务 |
#### Skill 定义格式说明
WaterFlow 的 SkillLoader 会从 `SKILL.md` 中提取:
- **name**: 从目录名提取(如 `graph_memory``persona``task`
- **description**: 从 Markdown 的第一个 `#` 标题提取
- **allowed-tools**: 转换为 `allowedTools` 字段
- **arguments**: 正确映射到 SkillDefinition.arguments
- **user-invocable**: 转换为 `userInvocable` 字段
**注意**: `when_to_use` 信息已整合到 Markdown body 中,通过 SkillRegistry.search() 可匹配。
---
## API
### GraphMemoryTool
```typescript
const tool = new GraphMemoryTool(sessionId?: string);
```
#### Actions
| Action | 说明 | 参数 |
|--------|------|------|
| `recall` | 检索记忆 | `queryIntent`, `seedEntities`, `depth`, `sessionFilter` |
| `commit` | 写入记忆 | `triplets`, `sessionId`, `turnId` |
| `purge` | 删除记忆 | `criteria`, `mode` |
| `introspect` | 查看状态 | - |
| `persona_update` | 更新人设 | `attributes`, `mode` |
| `persona_clear` | 清除人设 | `confirm` |
| `task_create` | 创建任务 | `task_id`, `description`, `info_nodes` |
| `task_set_state` | 设置状态 | `task_id`, `state` |
| `task_delete` | 删除任务 | `task_id` |
| `task_link_info` | 关联信息到任务 | `task_id`, `info_node` |
---
## 示例
### 写入记忆
```json
{
"action": "commit",
"params": {
"triplets": [
{ "subject": "用户", "relation": "喜欢", "object": "TypeScript" },
{ "subject": "用户", "relation": "正在学习", "object": "WaterFlow" }
]
}
}
```
### 检索记忆
```json
{
"action": "recall",
"params": {
"queryIntent": "用户 学习"
}
}
```
### 创建任务
```json
{
"action": "task_create",
"params": {
"task_id": "Task_学习TypeScript",
"description": "学习 TypeScript 并完成项目",
"info_nodes": ["文档链接", "教程链接"]
}
}
```
### 关联信息到任务
```json
{
"action": "task_link_info",
"params": {
"task_id": "Task_学习TypeScript",
"info_node": "用户喜欢 React"
}
}
```
---
## API 名称映射
OpenAI/DeepSeek API 要求工具名称符合 `^[a-zA-Z0-9_-]+$` 格式(不含冒号)。
内部工具 ID 使用 `builtin:xxx` 格式,需映射后发送给 API。
```typescript
import { mapToolIdToApiName, mapApiNameToToolId } from 'trulymem/tools';
// 发送给 API
const apiName = mapToolIdToApiName('builtin:graph_memory'); // -> 'graph_memory'
// 收到 tool_use 后映射回
const internalId = mapApiNameToToolId('graph_memory'); // -> 'builtin:graph_memory'
```
---
## 许可证
[GNU General Public License v3.0 (GPLv3)](LICENSE)

250
README_EN.md Normal file
View File

@ -0,0 +1,250 @@
# TrulyMEM - WaterFlow Adapter
Give AI true long-term memory capability - WaterFlow framework adapter version
[中文版本](./README.md)
---
## Introduction
This project ports TrulyMEM's graph memory capability to TypeScript for the WaterFlow framework.
As a built-in module for WaterFlow, it provides graph memory functionality:
- **recall**: Retrieve memories
- **commit**: Commit memories
- **purge**: Delete memories
- **introspect**: Inspect status
- **persona_update/clear**: Persona management
- **task_create/set_state/delete**: Task management
---
## Directory Structure
```
ts/
├── src/runtime/core/
│ ├── graph_memory/ # Graph memory core module
│ │ ├── types.ts # Type definitions
│ │ ├── graph_database.ts # Graph database
│ │ ├── memory_service.ts # Memory service
│ │ └── index.ts # Module exports
│ └── tools/
│ └── builtin/
│ └── graph_memory_tool.ts # Tool implementation
├── bundled-skills/ # Skill definitions
│ └── graph_memory/
│ ├── SKILL.md # Memory operations
│ ├── persona/SKILL.md # Persona management
│ └── task/SKILL.md # Task management
├── package.json # Project config
└── tsconfig.json # TypeScript config
```
---
## Usage in WaterFlow
This module requires **zero changes** to WaterFlow source code. Just register it in your entry file.
### Quick Start (Recommended)
#### Step 1: Install
```bash
npm install /path/to/TrulyMEM-TrueHumanMEM/ts
```
Or add to `package.json`:
```json
{
"dependencies": {
"trulymem-waterflow": "file:../TrulyMEM-TrueHumanMEM/ts"
}
}
```
Then run:
```bash
npm install
```
#### Step 2: Register in your entry file
Just two lines, zero changes to WaterFlow:
```typescript
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { installTrulyMEM } from 'trulymem/tools';
// One-line install, returns configured ToolRegistry
const registry = await installTrulyMEM(getPlatform(), 'my-session-id');
// Continue assembling WaterFlow...
const toolExecutor = new ToolExecutor(registry);
```
### Manual Registration (More control)
If you want to control ToolRegistry creation yourself:
```typescript
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { initializeToolRegistry } from 'waterflow-ts/dist/runtime/core/tools/builtin/index.js';
import { registerGraphMemoryTool } from 'trulymem/tools';
const platform = getPlatform();
const registry = initializeToolRegistry(platform);
// Register graph memory tool
registerGraphMemoryTool(registry, 'my-session-id');
// Continue assembling...
```
### Use Skill (AI Agent)
#### Step 1: Configure Skill source
```typescript
const config = {
...DEFAULT_SKILL_LOADER_CONFIG,
sources: {
...DEFAULT_SKILL_LOADER_CONFIG.sources,
bundled: './node_modules/trulymem-waterflow/bundled-skills'
},
enabledSources: ['project', 'bundled']
};
```
#### Step 2: Call via Agent
AI Agent automatically reads SKILL.md and calls `builtin:graph_memory` tool.
#### Available Skills
| Skill Name | Function | Use Case |
|------------|----------|----------|
| `graph_memory` | Memory CRUD | Read/Write/Delete memories |
| `persona` | Persona management | Set AI role/personality |
| `task` | Task management | Create/update long-term tasks |
#### Skill Definition Format
WaterFlow's SkillLoader extracts from `SKILL.md`:
- **name**: Extracted from directory name (e.g., `graph_memory`, `persona`, `task`)
- **description**: Extracted from first Markdown `#` heading
- **allowed-tools**: Converted to `allowedTools` field
- **arguments**: Properly mapped to SkillDefinition.arguments
- **user-invocable**: Converted to `userInvocable` field
**Note**: `when_to_use` info is integrated into Markdown body, searchable via SkillRegistry.search().
---
## API
### GraphMemoryTool
```typescript
const tool = new GraphMemoryTool(sessionId?: string);
```
#### Actions
| Action | Description | Parameters |
|--------|-------------|------------|
| `recall` | Retrieve memories | `queryIntent`, `seedEntities`, `depth`, `sessionFilter` |
| `commit` | Commit memories | `triplets`, `sessionId`, `turnId` |
| `purge` | Delete memories | `criteria`, `mode` |
| `introspect` | Inspect status | - |
| `persona_update` | Update persona | `attributes`, `mode` |
| `persona_clear` | Clear persona | `confirm` |
| `task_create` | Create task | `task_id`, `description`, `info_nodes` |
| `task_set_state` | Set state | `task_id`, `state` |
| `task_delete` | Delete task | `task_id` |
| `task_link_info` | Link info to task | `task_id`, `info_node` |
---
## Examples
### Commit Memory
```json
{
"action": "commit",
"params": {
"triplets": [
{ "subject": "User", "relation": "likes", "object": "TypeScript" },
{ "subject": "User", "relation": "is learning", "object": "WaterFlow" }
]
}
}
```
### Recall Memory
```json
{
"action": "recall",
"params": {
"queryIntent": "User learning"
}
}
```
### Create Task
```json
{
"action": "task_create",
"params": {
"task_id": "Task_LearnTypeScript",
"description": "Learn TypeScript and complete project",
"info_nodes": ["Documentation", "Tutorial"]
}
}
```
### Link Info to Task
```json
{
"action": "task_link_info",
"params": {
"task_id": "Task_LearnTypeScript",
"info_node": "User likes React"
}
}
```
---
## API Name Mapping
OpenAI/DeepSeek API requires tool names to match `^[a-zA-Z0-9_-]+$` (no colons).
Internal tool IDs use `builtin:xxx` format and must be mapped before sending to API.
```typescript
import { mapToolIdToApiName, mapApiNameToToolId } from 'trulymem/tools';
// Send to API
const apiName = mapToolIdToApiName('builtin:graph_memory'); // -> 'graph_memory'
// Map back after receiving tool_use
const internalId = mapApiNameToToolId('graph_memory'); // -> 'builtin:graph_memory'
```
---
## License
[GNU General Public License v3.0 (GPLv3)](LICENSE)

View File

@ -1,45 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = [('ui/styles', 'ui/styles'), ('core/prompts/templates', 'core/prompts/templates'), ('static', 'static'), ('templates', 'templates'), ('core/web_api.py', 'core/')]
binaries = []
hiddenimports = ['textual', 'textual.app', 'textual.widgets', 'textual.css', 'openai', 'openai._client', 'neo4j', 'sqlite3', 'core', 'core.embedded_db', 'core.graph_client', 'core.tool_executor', 'core.tool_limiter', 'core.tools', 'core.tools.memory_tools', 'core.prompts', 'core.prompts.prompt_manager', 'core.server', 'core.client', 'core.migrate', 'core.activity_recorder', 'ui', 'ui.app', 'ui.login_screen', 'ui.models', 'ui.models.message', 'ui.models.config', 'ui.models.log_entry', 'ui.widgets', 'ui.handlers', 'ui.services', 'ui.services.config_manager', 'ui.services.config_service', 'core.web_api', 'flask', 'flask_cors', 'werkzeug']
tmp_ret = collect_all('textual')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['trulymem_entry.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='TrulyMEM',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View File

@ -1,91 +0,0 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug"
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "common",
"srcPath": "./common",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "graph",
"srcPath": "./features/graph",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "chat",
"srcPath": "./features/chat",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "settings",
"srcPath": "./features/settings",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "phone",
"srcPath": "./products/phone",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
]
}

135
build.log
View File

@ -1,135 +0,0 @@
===== Building TrulyMEM for Linux =====
Project root: /home/program/TrulyMEM-TrueHumanMEM
The virtual environment was not created successfully because ensurepip is not
available. On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.
apt install python3.13-venv
You may need to use sudo with that command. After installing the python3-venv
package, recreate your virtual environment.
Failing command: /home/program/TrulyMEM-TrueHumanMEM/.venv_build/bin/python3
Warning: venv creation failed, falling back to system Python
Cleaning previous builds...
================================
Building TrulyMEM (TUI + Web embedded)
================================
29 INFO: PyInstaller: 6.20.0, contrib hooks: 2026.4
29 INFO: Python: 3.13.12
31 INFO: Platform: Linux-6.1.0-44-amd64-x86_64-with-glibc2.42
31 INFO: Python environment: /usr
33 INFO: Removing temporary files and cleaning cache in /root/.cache/pyinstaller
34 INFO: Module search paths (PYTHONPATH):
['/home/program/TrulyMEM-TrueHumanMEM',
'/home/program/TrulyMEM-TrueHumanMEM',
'/usr/lib/python313.zip',
'/usr/lib/python3.13',
'/usr/lib/python3.13/lib-dynload',
'/usr/local/lib/python3.13/dist-packages',
'/usr/lib/python3/dist-packages',
'/home/program/TrulyMEM-TrueHumanMEM']
141 INFO: Appending 'datas' from .spec
141 INFO: checking Analysis
141 INFO: Building Analysis because Analysis-00.toc is non existent
141 INFO: Looking for Python shared library...
149 INFO: Using Python shared library: /usr/lib/x86_64-linux-gnu/libpython3.13.so.1.0
149 INFO: Running Analysis Analysis-00.toc
149 INFO: Target bytecode optimization level: 0
149 INFO: Initializing module dependency graph...
149 INFO: Initializing module graph hook caches...
153 INFO: Analyzing modules for base_library.zip ...
645 INFO: Processing standard module hook 'hook-encodings.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
1604 INFO: Processing standard module hook 'hook-pickle.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2203 INFO: Processing standard module hook 'hook-heapq.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2434 INFO: Caching module dependency graph...
2456 INFO: Analyzing /home/program/TrulyMEM-TrueHumanMEM/trulymem_entry.py
2487 INFO: Processing standard module hook 'hook-sqlite3.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2606 INFO: Processing standard module hook 'hook-platform.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2628 INFO: Processing standard module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2631 INFO: Processing standard module hook 'hook-_ctypes.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2641 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
2641 INFO: SetuptoolsInfo: initializing cached setuptools info...
4602 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
4748 INFO: Processing standard module hook 'hook-xml.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
5155 INFO: Processing standard module hook 'hook-pydantic.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
5444 INFO: Processing standard module hook 'hook-rich.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
5729 INFO: Processing standard module hook 'hook-pygments.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
6155 INFO: Processing standard module hook 'hook-chardet.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
7547 INFO: Processing standard module hook 'hook-zoneinfo.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
8697 INFO: Processing standard module hook 'hook-certifi.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
8766 INFO: Processing standard module hook 'hook-anyio.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
9419 INFO: Processing standard module hook 'hook-difflib.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
10529 INFO: Processing standard module hook 'hook-numpy.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
11824 INFO: Processing standard module hook 'hook-charset_normalizer.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
12622 INFO: Processing standard module hook 'hook-pytz.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
13192 INFO: Processing pre-safe-import-module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
13199 INFO: Processing standard module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
14917 INFO: Processing standard module hook 'hook-jinja2.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
15284 INFO: Processing pre-safe-import-module hook 'hook-importlib_metadata.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15285 INFO: Setuptools: 'importlib_metadata' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.importlib_metadata'!
15289 INFO: Processing standard module hook 'hook-setuptools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
15296 INFO: Processing pre-safe-import-module hook 'hook-distutils.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15314 INFO: Processing pre-safe-import-module hook 'hook-jaraco.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15314 INFO: Setuptools: 'jaraco' appears to be a partial setuptools-vendored copy - extending search paths to ['/usr/lib/python3/dist-packages/jaraco', '/usr/lib/python3/dist-packages/setuptools/_vendor/jaraco']!
15315 INFO: Processing pre-safe-import-module hook 'hook-jaraco.functools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15320 INFO: Processing pre-safe-import-module hook 'hook-more_itertools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15459 INFO: Processing pre-safe-import-module hook 'hook-packaging.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15575 INFO: Processing pre-safe-import-module hook 'hook-jaraco.text.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15581 INFO: Processing standard module hook 'hook-jaraco.text.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
15614 INFO: Processing pre-safe-import-module hook 'hook-importlib_resources.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15615 INFO: Processing pre-safe-import-module hook 'hook-jaraco.context.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15620 INFO: Processing pre-safe-import-module hook 'hook-backports.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15620 INFO: Setuptools: 'backports' appears to be a full setuptools-vendored copy - creating alias to 'setuptools._vendor.backports'!
15843 INFO: Processing pre-safe-import-module hook 'hook-tomli.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15843 INFO: Setuptools: 'tomli' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.tomli'!
16138 INFO: Processing standard module hook 'hook-pkg_resources.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
16316 INFO: Processing pre-safe-import-module hook 'hook-wheel.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
16376 INFO: Processing standard module hook 'hook-setuptools._vendor.importlib_metadata.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
16378 INFO: Processing pre-safe-import-module hook 'hook-zipp.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
16414 INFO: Analyzing hidden import 'ui.handlers'
16414 INFO: Analyzing hidden import 'ui.services'
16415 INFO: Analyzing hidden import 'ui.services.config_manager'
16416 INFO: Analyzing hidden import 'ui.services.config_service'
16417 INFO: Processing module hooks (post-graph stage)...
16716 WARNING: Hidden import "charset_normalizer.md__mypyc" not found!
18203 INFO: Performing binary vs. data reclassification (622 entries)
18209 INFO: Looking for ctypes DLLs
18283 WARNING: Library shell32 required via ctypes not found
18292 WARNING: Library ole32 required via ctypes not found
18321 INFO: Analyzing run-time hooks ...
18329 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
18331 INFO: Including run-time hook 'pyi_rth_multiprocessing.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
18333 INFO: Including run-time hook 'pyi_rth_pkgutil.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
18334 INFO: Including run-time hook 'pyi_rth_setuptools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
18335 INFO: Including run-time hook 'pyi_rth_pkgres.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/rthooks'
18371 INFO: Creating base_library.zip...
18384 INFO: Looking for dynamic libraries
18673 INFO: Warnings written to /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/warn-trulymem.txt
18783 INFO: Graph cross-reference written to /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/xref-trulymem.html
18816 INFO: checking PYZ
18816 INFO: Building PYZ because PYZ-00.toc is non existent
18816 INFO: Building PYZ (ZlibArchive) /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/PYZ-00.pyz
19768 INFO: Building PYZ (ZlibArchive) /home/program/TrulyMEM-TrueHumanMEM/build/trulymem/PYZ-00.pyz completed successfully.
19790 WARNING: Ignoring icon; supported only on Windows and macOS!
19801 INFO: checking PKG
19801 INFO: Building PKG because PKG-00.toc is non existent
19801 INFO: Building PKG (CArchive) TrulyMEM.pkg
24763 INFO: Building PKG (CArchive) TrulyMEM.pkg completed successfully.
24768 INFO: Bootloader /usr/local/lib/python3.13/dist-packages/PyInstaller/bootloader/Linux-64bit-intel/run
24768 INFO: checking EXE
24768 INFO: Building EXE because EXE-00.toc is non existent
24768 INFO: Building EXE from EXE-00.toc
24768 INFO: Copying bootloader EXE to /home/program/TrulyMEM-TrueHumanMEM/dist/TrulyMEM
24768 INFO: Appending PKG archive to custom ELF section in EXE
24825 INFO: Building EXE from EXE-00.toc completed successfully.
24830 INFO: Build complete! The results are available in: /home/program/TrulyMEM-TrueHumanMEM/dist
================================
===== Build Complete =====
Binary: dist/TrulyMEM
total 35848
drwxr-xr-x 2 root root 4096 Apr 30 07:06 .
drwxr-xr-x 15 root root 4096 Apr 30 07:05 ..
-rwxr-xr-x 1 root root 36698096 Apr 30 07:06 TrulyMEM
Build finished successfully!

View File

@ -1,32 +0,0 @@
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@security/no-unsafe-aes": "error",
"@security/no-unsafe-hash": "error",
"@security/no-unsafe-mac": "warn",
"@security/no-unsafe-dh": "error",
"@security/no-unsafe-dsa": "error",
"@security/no-unsafe-ecdsa": "error",
"@security/no-unsafe-rsa-encrypt": "error",
"@security/no-unsafe-rsa-sign": "error",
"@security/no-unsafe-rsa-key": "error",
"@security/no-unsafe-dsa-key": "error",
"@security/no-unsafe-dh-key": "error",
"@security/no-unsafe-3des": "error"
}
}

View File

@ -1,17 +0,0 @@
/**
* 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,24 +0,0 @@
// ========= 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";

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
{
"name": "@ohos/common",
"version": "1.0.0",
"description": "TrulyMEM common module",
"main": "Index.ets",
"author": "",
"license": "",
"dependencies": {}
}

View File

@ -1,133 +0,0 @@
import { defaultLogger } from '../util/Logger';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
const THEME_COLOR = '#7C4DFF';
@Component
export struct ImmersiveTabNavigation {
@State currentIndex: number = 0;
@BuilderParam contentBuilder: () => void;
onTabChange?: (index: number) => void;
private windowFocused: boolean = true;
private bottomAvoidHeight: number = 0;
aboutToAppear() {
const mainWindow = AppStorage.get<window.Window>('main_window');
if (mainWindow) {
try {
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
this.bottomAvoidHeight = avoidArea.bottomRect.height || 0;
} catch (e) {
defaultLogger.error('Failed to get avoid area: ' + (e as BusinessError).message);
}
}
}
triggerTabSwitchFeedback(index: number) {
this.currentIndex = index;
AppStorage.setOrCreate('global_theme_color', THEME_COLOR);
this.onTabChange?.(index);
}
@Builder
tabBarBuilder(index: number, icon: string, label: string) {
Column() {
if (this.currentIndex === index && this.windowFocused) {
Circle()
.width(32)
.height(32)
.backgroundColor(`${THEME_COLOR}33`)
.blur(8)
.position({ x: '50%', y: '50%' })
.translate({ x: '-50%', y: '-50%' })
}
Text(icon)
.fontSize(20)
.opacity(this.currentIndex === index ? 1 : 0.5)
Text(label)
.fontSize(10)
.fontColor(this.currentIndex === index ? THEME_COLOR : '#999')
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
build() {
Stack() {
Column() {
this.contentBuilder()
}
.width('100%')
.height('100%')
Column() {
Stack() {
Column()
.width('100%')
.height('100%')
.backgroundBlurStyle(BlurStyle.Regular)
.borderRadius(24)
Column()
.width('100%')
.height('100%')
.backgroundColor(`${THEME_COLOR}0D`)
.borderRadius(24)
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['rgba(255,255,255,0.15)', 0.0], ['rgba(255,255,255,0.05)', 1.0]]
})
.borderRadius(24)
}
.width('100%')
.height('100%')
Tabs({ index: this.currentIndex }) {
TabContent() {
Column() {
Blank()
}
}
.tabBar(this.tabBarBuilder(0, '🌌', 'TrulyMEM'))
TabContent() {
Column() {
Blank()
}
}
.tabBar(this.tabBarBuilder(1, '⚙', '设置'))
}
.width('100%')
.height(64)
.barPosition(BarPosition.End)
.onChange((index: number) => {
this.triggerTabSwitchFeedback(index);
})
}
.width('92%')
.height(72)
.alignSelf(ItemAlign.Center)
.position({ y: `calc(100% - ${this.bottomAvoidHeight > 0 ? this.bottomAvoidHeight : 16}px - 72px)` })
.borderRadius(24)
.shadow({
radius: 20,
offsetY: -4,
color: 'rgba(0,0,0,0.15)'
})
}
.width('100%')
.height('100%')
.backgroundColor('#00000000')
}
}

View File

@ -1,44 +0,0 @@
export class Constants {
static readonly DB_NAME: string = 'trulymem.db';
static readonly CONFIG_PREF_NAME: string = 'trulymem_config';
static readonly DEFAULT_BASE_URL: string = 'https://api.deepseek.com';
static readonly DEFAULT_MODEL: string = 'deepseek-chat';
static readonly SECURITY_LEVEL: number = 1; // S1
// Table names
static readonly TABLE_NODES: string = 'nodes';
static readonly TABLE_RELATIONS: string = 'relations';
static readonly TABLE_CHAT: string = 'chat_records';
// SQL definitions
static readonly SQL_CREATE_NODES: string = `
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT DEFAULT 'concept',
mentions INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT DEFAULT (datetime('now','localtime'))
)`;
static readonly SQL_CREATE_RELATIONS: string = `
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id INTEGER NOT NULL,
relation TEXT NOT NULL,
object_id INTEGER NOT NULL,
weight REAL DEFAULT 1.0,
created_at TEXT DEFAULT (datetime('now','localtime')),
FOREIGN KEY (subject_id) REFERENCES nodes(id) ON DELETE CASCADE,
FOREIGN KEY (object_id) REFERENCES nodes(id) ON DELETE CASCADE
)`;
static readonly SQL_CREATE_CHAT: string = `
CREATE TABLE IF NOT EXISTS chat_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
tools TEXT,
created_at TEXT DEFAULT (datetime('now','localtime'))
)`;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,936 +0,0 @@
import relationalStore from '@ohos.data.relationalStore';
import { Context } from '@ohos.abilityAccessCtrl';
interface NodeNameCacheItem { name: string; type: string; mentions: number; }
export interface TimeRangeParams { days: number; }
interface TripletData {
subject: string;
relation: string;
object: string;
}
interface CriteriaData {
subject?: string;
target?: string;
relation?: string;
sessionId?: string;
}
interface NodeData {
id: number;
label: string;
type: string;
mentions: number;
depth?: number;
}
interface EdgeData {
from: number;
to: number;
label: string;
weight: number;
depth?: number;
sessionId?: string;
turnId?: number;
}
interface GraphData {
nodes: NodeData[];
edges: EdgeData[];
}
export interface RecallEntity {
name: string;
type: string;
mention_count: number;
depth?: number;
}
interface RecallRelation {
source: string;
target: string;
type: string;
confidence: number;
session_id?: string;
turn_id?: number;
depth?: number;
}
interface RecallResult {
entities: RecallEntity[];
relations: RecallRelation[];
message: string;
}
interface BfsEntity {
id: number;
name: string;
type: string;
mentions: number;
depth: number;
}
export interface RelationQueryResult {
sourceId: number;
targetId: number;
sourceName: string;
targetName: string;
type: string;
confidence: number;
sessionId?: string;
turnId?: number;
depth: number;
}
interface NodeQueryResult {
id: number;
name: string;
type: string;
mentions: number;
depth: number;
}
interface CleanupResult {
cleaned: number;
deleted_relations?: number;
deleted_orphans?: number;
dry_run?: boolean;
message?: string;
}
interface IntrospectResult {
entity_count: number;
relation_count: number;
message: string;
}
interface ArchiveResult {
archived: number;
message: string;
}
interface SearchResultItem {
name: string;
type: string;
mentions: number;
}
interface ChatMessage {
role: string;
content: string;
session_id?: string;
}
interface SnapshotData {
entities: RecallEntity[];
relations: RecallRelation[];
}
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'trulymem.db',
securityLevel: relationalStore.SecurityLevel.S1
};
export class GraphDatabase {
private store?: relationalStore.RdbStore;
private context?: Context;
async init(context: Context): Promise<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,56 +0,0 @@
import { defaultLogger } from '../util/Logger';
export interface RouterParam {
routerName: string;
param?: object;
}
export interface IPageContext {
openPage(data: RouterParam, animated?: boolean): void;
popPage(animated?: boolean): void;
replacePage(data: RouterParam, animated?: boolean): void;
}
export class PageContext implements IPageContext {
private readonly pathStack: NavPathStack;
constructor() {
this.pathStack = new NavPathStack();
}
public get navPathStack(): NavPathStack {
return this.pathStack;
}
public replacePage(data: RouterParam, animated: boolean = true): void {
try {
this.pathStack.replacePath({ name: data.routerName, param: data.param }, animated);
} catch (err) {
defaultLogger.error('replacePage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
}
}
public openPage(data: RouterParam, animated: boolean = true): void {
try {
this.pathStack.pushPath({ name: data.routerName, param: data.param }, animated);
} catch (err) {
defaultLogger.error('openPage: ' + data.routerName + ' failed. ' + err.code + ' ' + err.message);
}
}
public popPage(animated: boolean = true): void {
try {
this.pathStack.pop(animated);
} catch (err) {
defaultLogger.error('popPage failed. ' + err.code + ' ' + err.message);
}
}
public popPageByIndex(index: number, animated: boolean = true): void {
this.pathStack.popToIndex(index, animated);
}
public clear(animated: boolean = true): void {
this.pathStack.clear(animated);
}
}

View File

@ -1,891 +0,0 @@
/**
* AIAgentService - AI Agent 服务层
* 管理上下文感知的 AI 对话,注入图数据作为上下文,
* 解析 AI 返回中的记忆操作,调用 GraphMemoryService 执行
* 参考main 分支 core/graph_client.py
*/
import http from '@ohos.net.http';
import dataPreferences from '@ohos.data.preferences';
import { Context } from '@ohos.abilityAccessCtrl';
import { GraphMemoryService, EntityInfo, RelationInfo, TaskInfo, MemoryRecallParams, MemoryCommitParams, MemoryPurgeParams, PurgeCriteriaParams, NewRelationParams, PersonaUpdateParams, TaskCreateParams, TaskSetStateParams, TaskDeleteParams, TaskLinkInfoParams, TaskArchiveParams, TaskQueryParams, TripletInput, PersonaQueryResult, TaskQueryResult, MemoryRecallResult } from './GraphMemoryService';
import { TimeRangeParams } from '../model/GraphDatabase';
export interface ChatMessage {
role: string;
content: string;
}
export interface AgentResponse {
content: string;
toolCalls: ToolCallResult[];
}
export interface ToolCallResult {
name: string;
success: boolean;
message: string;
}
// ========= 工具定义类型 =========
// Concrete interface for tool property definitions (replaces Record<string, T>)
interface ToolPropertiesDefinition {
days?: ToolParamProperty;
queryIntent?: ToolParamProperty;
seedEntities?: ToolParamProperty;
depth?: ToolParamProperty;
timeRange?: ToolParamProperty;
sessionFilter?: ToolParamProperty;
triplets?: ToolParamProperty;
entityTypes?: ToolParamProperty;
sessionId?: ToolParamProperty;
turnId?: ToolParamProperty;
criteria?: ToolParamProperty;
mode?: ToolParamProperty;
newRelation?: ToolParamProperty;
tone?: ToolParamProperty;
style?: ToolParamProperty;
personality?: ToolParamProperty;
catchphrase?: ToolParamProperty;
background?: ToolParamProperty;
taskId?: ToolParamProperty;
description?: ToolParamProperty;
infoNodes?: ToolParamProperty;
state?: ToolParamProperty;
deleteInfoNodes?: ToolParamProperty;
infoNodeNames?: ToolParamProperty;
summary?: ToolParamProperty;
limit?: ToolParamProperty;
stateFilter?: ToolParamProperty;
subject?: ToolParamProperty;
relation?: ToolParamProperty;
object?: ToolParamProperty;
confidence?: ToolParamProperty;
subjectContains?: ToolParamProperty;
relationType?: ToolParamProperty;
targetContains?: ToolParamProperty;
target?: ToolParamProperty;
dryRun?: ToolParamProperty;
keyword?: ToolParamProperty;
attribute?: ToolParamProperty;
sourceType?: ToolParamProperty;
targetType?: ToolParamProperty;
sourceHasStatus?: ToolParamProperty;
}
interface ToolParamProperty {
type: string;
description: string;
items?: ToolParamProperty;
properties?: ToolPropertiesDefinition;
required?: string[];
enum?: string[];
}
interface ToolParamDecl {
type: string;
properties: ToolPropertiesDefinition;
required?: string[];
}
interface ToolFunctionDecl {
name: string;
description: string;
parameters: ToolParamDecl;
}
interface ToolFunctionDef {
type: string;
function: ToolFunctionDecl;
}
// ========= API 请求/响应结构 =========
interface ApiRequestMessage {
role: string;
content: string;
}
interface ApiRequest {
model: string;
messages: ApiRequestMessage[];
tools?: ToolFunctionDef[];
tool_choice?: string;
}
interface ApiToolCall {
id: string;
type: string;
function: ToolFunctionCall;
}
interface ToolFunctionCall {
name: string;
arguments: string;
}
interface ApiChoiceMessage {
content?: string;
tool_calls?: ApiToolCall[];
}
interface ApiChoice {
message: ApiChoiceMessage;
}
interface ApiResponse {
choices: ApiChoice[];
}
// ========= 内部结果类型 =========
interface ExecuteToolResult {
name: string;
success: boolean;
message: string;
}
interface BuildContextBlockParams {
persona: Record<string, string>;
found: boolean;
entities: EntityInfo[];
relations: RelationInfo[];
message: string;
}
// ========= 服务方法参数类型 =========
// ========= 工具定义辅助函数 =========
function makeStringProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'string', description: description };
return result;
}
function makeIntegerProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'integer', description: description };
return result;
}
function makeObjectProp(description: string, props: ToolPropertiesDefinition, required?: string[]): ToolParamProperty {
const param: ToolParamProperty = { type: 'object', description: description };
param.properties = props;
if (required && required.length > 0) {
param.required = required;
}
return param;
}
function makeArrayProp(description: string, item: ToolParamProperty): ToolParamProperty {
const result: ToolParamProperty = { type: 'array', description: description, items: item };
return result;
}
function makeBoolProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'boolean', description: description };
return result;
}
function makeNumberProp(description: string): ToolParamProperty {
const result: ToolParamProperty = { type: 'number', description: description };
return result;
}
function makeEnumProp(description: string, enumValues: string[]): ToolParamProperty {
const result: ToolParamProperty = { type: 'string', description: description, enum: enumValues };
return result;
}
function makeToolDef(name: string, description: string, properties: ToolPropertiesDefinition, required?: string[]): ToolFunctionDef {
const params: ToolParamDecl = { type: 'object', properties: properties };
const func: ToolFunctionDecl = { name: name, description: description, parameters: params };
const tool: ToolFunctionDef = { type: 'function', function: func };
if (required && required.length > 0) {
tool.function.parameters.required = required;
}
return tool;
}
/**
* 系统提示词 — AI 人设 + 图记忆使用说明
*/
function buildSystemPrompt(personaContext: string): string {
return `你是 TrulyMEMTrue Memory——一个拥有真实记忆的 AI 助手。
## 核心身份
${personaContext || '你是一个帮助用户记录和回忆信息的助手。你的核心能力是基于图数据库的记忆系统。'}
## ⚠️ 内部执行顺序(不得向用户输出)
以下步骤是内部流程,绝对不要在你的回复中提及或输出。应当仅通过工具调用悄悄完成,回复时直接给出自然的对话内容。
1. memory_recall → 查询人设图
2. task_query → 查询工作记忆链/最近任务
3. 处理对话内容 + 思考回复
4. memory_commit → 写入本轮关键信息到图数据库
5. task_archive → 归档已完成的旧任务
6. 条件: 本轮调用 ≥5 次查询类工具 → context_rewrite 压缩工具 JSON
## 三元组规范
使用 memory_commit 时subject/relation/object 每个字段必须是一个短关键字1~5个字不能是完整句子。
## 任务信息节点规范
- info_nodes 只能包含该任务专属的具体信息节点,严禁关联"用户"、"AI"、"系统"等全局通用实体
- 全局实体的信息直接用独立关系记录,不需要通过 Task 中转
## 可用工具
- memory_recall(queryIntent, seedEntities?, depth?, timeRange?, sessionFilter?): 检索记忆
- memory_commit(triplets, entityTypes?, sessionId?, turnId?): 写入记忆
- memory_purge(criteria, mode, newRelation?): 删除/修正记忆
- memory_introspect(sessionId?): 查看记忆状态统计
- memory_archive(days?): 归档旧记忆
- memory_cleanup(dryRun?): 清理已删除数据
- memory_query_archived(days?, keyword?): 查询已归档记忆
- context_rewrite(summary): 压缩工具调用上下文
- persona_update(tone?, style?, personality?, catchphrase?, background?): 更新人设
- persona_remove(attribute): 删除单条人设属性
- persona_clear(): 清除人设
- task_create(taskId, description, infoNodes?): 创建任务
- task_set_state(taskId, state): 设置任务状态
- task_delete(taskId, deleteInfoNodes?): 删除任务
- task_link_info(taskId, infoNodeNames): 关联信息节点
- task_archive(taskId, summary?): 归档任务
- task_query(limit?, stateFilter?): 查询任务列表
## 工具调用规则
1. ⚠️ 在完成所有工具调用之前,绝对不要输出任何文字。先默默调用工具,等所有结果返回后再输出一次完整的回复。
2. 每轮对话必须按顺序执行步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求
3. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!
4. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文
## 写入规则
用户明确表达以下信息时必须写入记忆:
- 偏好、兴趣
- 个人信息(工作、项目、学习)
- 计划安排
- 当前状态
- 结论性事实
推理得到的信息可以写入但需标注 [推测]。`;
}
// ========= 工具定义 =========
// Pre-typed property dictionaries for tool definitions
const recallTimeRangeDict: ToolPropertiesDefinition = { days: makeIntegerProp('最近N天') };
const tripletPropsDict: ToolPropertiesDefinition = {
subject: makeStringProp('主体'),
relation: makeStringProp('关系'),
object: makeStringProp('客体'),
confidence: makeNumberProp('置信度')
};
const purgeCriteriaDict: ToolPropertiesDefinition = {
subjectContains: makeStringProp('源实体名包含(模糊匹配)'),
relationType: makeStringProp('关系类型'),
targetContains: makeStringProp('目标实体名包含(模糊匹配)'),
sessionId: makeStringProp('会话ID过滤'),
sourceType: makeStringProp('源实体类型过滤(如 TaskNode'),
targetType: makeStringProp('目标实体类型过滤'),
sourceHasStatus: makeStringProp('源实体状态过滤(如 archived')
};
const newRelDict: ToolPropertiesDefinition = {
relation: makeStringProp(''),
target: makeStringProp('')
};
const EMPTY_PROPS: ToolPropertiesDefinition = {};
const recallProps: ToolPropertiesDefinition = {
queryIntent: makeStringProp('查询意图,支持逗号分隔多个关键词'),
seedEntities: makeArrayProp('种子实体(可选)', makeStringProp('')),
depth: makeIntegerProp('搜索深度默认2'),
timeRange: makeObjectProp('时间范围(可选)', recallTimeRangeDict),
sessionFilter: makeStringProp('会话ID过滤可选')
};
const commitProps: ToolPropertiesDefinition = {
triplets: makeArrayProp('三元组列表', makeObjectProp('', tripletPropsDict, ['subject', 'relation', 'object'])),
entityTypes: makeObjectProp('实体类型映射(可选)', EMPTY_PROPS),
sessionId: makeStringProp('会话ID可选'),
turnId: makeIntegerProp('轮次ID可选')
};
const purgeProps: ToolPropertiesDefinition = {
criteria: makeObjectProp('删除条件', purgeCriteriaDict),
mode: makeEnumProp('删除模式soft逻辑删除, hard物理删除, supersede纠错替代', ['soft', 'hard', 'supersede']),
newRelation: makeObjectProp('替代关系supersede模式用', newRelDict)
};
const personaProps: ToolPropertiesDefinition = {
tone: makeStringProp('语气'),
style: makeStringProp('风格'),
personality: makeStringProp('性格'),
catchphrase: makeStringProp('口头禅'),
background: makeStringProp('背景')
};
const createProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
description: makeStringProp('任务描述'),
infoNodes: makeArrayProp('关联的信息节点名称列表', makeStringProp(''))
};
const setStateProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
state: makeEnumProp('任务状态', ['进行中', '已完成', '已暂停', '已取消'])
};
const deleteProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
deleteInfoNodes: makeBoolProp('是否删除关联的信息节点')
};
const linkInfoProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
infoNodeNames: makeArrayProp('信息节点名称列表', makeStringProp(''))
};
const archiveProps: ToolPropertiesDefinition = {
taskId: makeStringProp('任务ID'),
summary: makeStringProp('归档摘要')
};
const queryProps: ToolPropertiesDefinition = {
limit: makeIntegerProp('返回数量默认10'),
stateFilter: makeStringProp('状态过滤: 进行中/已完成/已暂停/已取消/archived')
};
const introspectProps: ToolPropertiesDefinition = {
sessionId: makeStringProp('会话ID可选')
};
const archiveProps2: ToolPropertiesDefinition = {
days: makeIntegerProp('归档天数默认30')
};
const cleanupProps: ToolPropertiesDefinition = {
dryRun: makeBoolProp('仅预览不删除')
};
const queryArchivedProps: ToolPropertiesDefinition = {
days: makeIntegerProp('最近N天内的归档记录'),
keyword: makeStringProp('关键词过滤')
};
const contextRewriteProps: ToolPropertiesDefinition = {
summary: makeStringProp('压缩后的摘要文本,必须包含工具调用元信息')
};
const personaRemoveProps: ToolPropertiesDefinition = {
attribute: makeStringProp('要删除的属性名(如:扮演角色、说话风格)')
};
const TOOLS_DEFINITION: ToolFunctionDef[] = [
makeToolDef('memory_recall', '检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。\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', '查询已归档的记忆。\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', '创建新的工作记忆任务节点。\n\n【重要】info_nodes 只能包含该任务专属的具体信息节点(如\"成语接龙_当前成语\"**严禁关联\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', createProps, ['taskId', 'description']),
makeToolDef('task_set_state', '设置任务状态。', setStateProps, ['taskId', 'state']),
makeToolDef('task_delete', '删除任务节点。', deleteProps, ['taskId']),
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)
];
// ========= 工具名称映射 =========
// Types for executeTool generic args
type ToolStateArg = '进行中' | '已完成' | '已暂停' | '已取消';
type ToolHandlerName =
| 'memoryRecal'
| 'memoryCommit'
| 'memoryPurge'
| 'memoryIntrospect'
| 'memoryArchive'
| 'memoryCleanup'
| 'memoryQueryArchived'
| 'contextRewrite'
| 'personaUpdate'
| 'personaRemove'
| 'personaClear'
| 'taskCreate'
| 'taskSetState'
| 'taskDelete'
| 'taskLinkInfo'
| 'taskArchive'
| 'taskQuery';
const TOOL_HANDLER_MAP: Record<string, ToolHandlerName> = {
'memory_recall': 'memoryRecal',
'memory_commit': 'memoryCommit',
'memory_purge': 'memoryPurge',
'memory_introspect': 'memoryIntrospect',
'memory_archive': 'memoryArchive',
'memory_cleanup': 'memoryCleanup',
'memory_query_archived': 'memoryQueryArchived',
'context_rewrite': 'contextRewrite',
'persona_update': 'personaUpdate',
'persona_remove': 'personaRemove',
'persona_clear': 'personaClear',
'task_create': 'taskCreate',
'task_set_state': 'taskSetState',
'task_delete': 'taskDelete',
'task_link_info': 'taskLinkInfo',
'task_archive': 'taskArchive',
'task_query': 'taskQuery',
};
// ========= AIAgentService =========
export class AIAgentService {
private memoryService: GraphMemoryService;
private currentSessionId: string;
private turnCounter: number = 0;
private appContext: Context;
constructor(memoryService: GraphMemoryService, appContext: Context, sessionId?: string) {
this.memoryService = memoryService;
this.appContext = appContext;
this.currentSessionId = sessionId || `session-hm-${Date.now()}`;
}
getSessionId(): string {
return this.currentSessionId;
}
/**
* 发送消息 — 完整的 Agent 流程
* 1. 查询人设
* 2. 查询工作记忆链
* 3. 注入上下文后请求 AI
* 4. 处理 tool_calls
* 5. 返回最终回复
*/
async sendMessage(userInput: string): Promise<AgentResponse> {
this.turnCounter++;
// === 步骤1+2: 获取上下文 ===
const personaResult = await this.memoryService.personaQuery();
const personaContext: string = personaResult.found ? this.formatPersona(personaResult.persona) : '';
const recallParams: MemoryRecallParams = {
queryIntent: 'TaskNode,工作记忆,任务链',
depth: 2
};
const taskResult = await this.memoryService.memoryRecall(recallParams);
// === 读取 API 配置 ===
const context = this.appContext;
const pref = await dataPreferences.getPreferences(context, 'trulymem_config');
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
const model: string = String(await pref.get('model', 'deepseek-chat'));
const apiKey: string = String(await pref.get('api_key', ''));
if (!apiKey) {
const noKeyResponse: AgentResponse = {
content: '⚠️ API Key 未配置,请先在设置页填写 API Key。',
toolCalls: []
};
return noKeyResponse;
}
// === 构建上下文丰富的消息 ===
const systemPrompt: string = buildSystemPrompt(personaContext);
const contextBlock: string = this.buildContextBlock(personaResult, taskResult);
const sysMsg: ApiRequestMessage = { role: 'system' as string, content: systemPrompt };
const userMsg: ApiRequestMessage = { role: 'user' as string, content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
const messages: ApiRequestMessage[] = [sysMsg, userMsg];
// === 步骤3: 请求 AI ===
const response: ApiResponse = await this.callApi(messages, baseUrl, model, apiKey);
const toolCalls: ToolCallResult[] = [];
// === 步骤4: 处理 tool_calls ===
if (response.choices && response.choices.length > 0) {
const choice: ApiChoice = response.choices[0];
const aiMessage: ApiChoiceMessage = choice.message;
// 处理函数调用
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
for (const tc of aiMessage.tool_calls) {
const handlerName: ToolHandlerName | undefined = TOOL_HANDLER_MAP[tc.function.name];
if (handlerName) {
const args: Record<string, Object> = JSON.parse(tc.function.arguments);
const result: ToolCallResult = await this.executeTool(handlerName, args);
toolCalls.push(result);
} else {
const unknownToolResult: ToolCallResult = {
name: tc.function.name,
success: false,
message: '未知工具'
};
toolCalls.push(unknownToolResult);
}
}
// 有 tool_calls 时需要再次请求 AI带上工具执行结果
const followUpSystemMsg: ApiRequestMessage = { role: 'system', content: systemPrompt };
const followUpUserMsg: ApiRequestMessage = { role: 'user', content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
const followUpAssistantMsg: ApiRequestMessage = {
role: 'assistant',
content: aiMessage.content || '(已执行记忆操作)',
};
const toolResultsMessages: ApiRequestMessage[] = [
followUpSystemMsg,
followUpUserMsg,
followUpAssistantMsg,
];
for (const tc of aiMessage.tool_calls) {
const callResult: ToolCallResult | undefined = toolCalls.find(r => r.name === tc.function.name);
const toolResultMsg: string = callResult ? callResult.message : '完成';
const toolResultMessage: ApiRequestMessage = {
role: 'tool',
content: `工具 ${tc.function.name} 执行结果: ${toolResultMsg}`
};
toolResultsMessages.push(toolResultMessage);
}
const finalResponse: ApiResponse = await this.callApi(toolResultsMessages, baseUrl, model, apiKey);
if (finalResponse.choices && finalResponse.choices.length > 0) {
const content: string = finalResponse.choices[0].message.content || '';
const finalResult: AgentResponse = { content, toolCalls };
return finalResult;
}
}
// 普通回复(无 tool_calls
const content: string = aiMessage.content || '';
const noToolResponse: AgentResponse = { content, toolCalls };
return noToolResponse;
}
const noResponse: AgentResponse = {
content: 'AI 无响应',
toolCalls
};
return noResponse;
}
/**
* 请求 DeepSeek API
*/
private async callApi(
messages: ApiRequestMessage[],
baseUrl: string,
model: string,
apiKey: string
): Promise<ApiResponse> {
const httpRequest = http.createHttp();
try {
const resp = await httpRequest.request(baseUrl + '/chat/completions', {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
extraData: {
model: model,
messages: messages,
tools: TOOLS_DEFINITION,
tool_choice: 'auto'
},
expectDataType: http.HttpDataType.OBJECT,
readTimeout: 60000
});
if (resp.responseCode === 200) {
return resp.result as ApiResponse;
}
const errorMsg: string = `API 请求失败: HTTP ${resp.responseCode}`;
throw new Error(errorMsg);
} finally {
httpRequest.destroy();
}
}
/**
* 执行工具调用
*/
private async executeTool(name: ToolHandlerName, args: Record<string, Object>): Promise<ToolCallResult> {
try {
switch (name) {
case 'memoryRecal': {
const recallArgs: MemoryRecallParams = {
queryIntent: args.queryIntent as string,
seedEntities: args.seedEntities as string[],
depth: (args.depth as number) ?? 2,
timeRange: args.timeRange as TimeRangeParams,
sessionFilter: args.sessionFilter as string
};
const recallResult = await this.memoryService.memoryRecall(recallArgs);
const result: ToolCallResult = {
name: 'memory_recall',
success: true,
message: `找到 ${recallResult.entities.length} 个实体, ${recallResult.relations.length} 条关系`
};
return result;
}
case 'memoryCommit': {
const commitParams: MemoryCommitParams = {
triplets: args.triplets as TripletInput[],
entityTypes: args.entityTypes as Record<string, string>,
sessionId: (args.sessionId as string) || this.currentSessionId,
turnId: (args.turnId as number) || this.turnCounter
};
const commitResult = await this.memoryService.memoryCommit(commitParams);
const result: ToolCallResult = {
name: 'memory_commit',
success: true,
message: `已写入 ${commitResult.committedCount} 条记忆`
};
return result;
}
case 'memoryPurge': {
const purgeArgs: MemoryPurgeParams = {
criteria: args.criteria as PurgeCriteriaParams,
mode: args.mode as 'soft' | 'hard' | 'supersede',
newRelation: args.newRelation as NewRelationParams
};
const purgeResult = await this.memoryService.memoryPurge(purgeArgs);
const result: ToolCallResult = {
name: 'memory_purge',
success: true,
message: purgeResult.message
};
return result;
}
case 'memoryIntrospect': {
const introspectResult = await this.memoryService.memoryIntrospect(args.sessionId as string);
const result: ToolCallResult = {
name: 'memory_introspect',
success: true,
message: `实体: ${introspectResult.entityCount}, 关系: ${introspectResult.relationCount}, 热点: ${introspectResult.hotNodes.length}`
};
return result;
}
case 'memoryArchive': {
const archiveResult = await this.memoryService.archive(args.days as number);
const result: ToolCallResult = {
name: 'memory_archive',
success: true,
message: `已归档 ${archiveResult.archived} 条关系`
};
return result;
}
case 'memoryCleanup': {
const cleanupResult = await this.memoryService.cleanup((args.dryRun as boolean) !== false);
const result: ToolCallResult = {
name: 'memory_cleanup',
success: true,
message: `清理: ${cleanupResult.cleaned} 条关系, ${cleanupResult.deletedOrphans} 个孤儿节点` + (cleanupResult.dryRun ? ' (预览模式)' : '')
};
return result;
}
case 'memoryQueryArchived': {
const qaResult = await this.memoryService.queryArchived(args.days as number, args.keyword as string);
const result: ToolCallResult = {
name: 'memory_query_archived',
success: true,
message: `找到 ${qaResult.length} 条归档记录`
};
return result;
}
case 'contextRewrite': {
const summary = args.summary as string;
const result: ToolCallResult = {
name: 'context_rewrite',
success: summary.includes('[工具调用总结'),
message: summary.includes('[工具调用总结') ? '上下文已压缩' : '格式错误:必须包含[工具调用总结]标记'
};
return result;
}
case 'personaUpdate': {
const personaParams: PersonaUpdateParams = {
tone: args.tone as string,
style: args.style as string,
personality: args.personality as string,
catchphrase: args.catchphrase as string,
background: args.background as string
};
const puResult = await this.memoryService.personaUpdate(personaParams);
const result: ToolCallResult = {
name: 'persona_update',
success: puResult.success,
message: puResult.message
};
return result;
}
case 'personaRemove': {
const prResult = await this.memoryService.personaRemove(args.attribute as string);
const result: ToolCallResult = {
name: 'persona_remove',
success: prResult.success,
message: prResult.message
};
return result;
}
case 'personaClear': {
const pcResult = await this.memoryService.personaClear();
const result: ToolCallResult = {
name: 'persona_clear',
success: pcResult.success,
message: pcResult.message
};
return result;
}
case 'taskCreate': {
const createParams: TaskCreateParams = {
taskId: args.taskId as string,
description: args.description as string,
infoNodes: args.infoNodes as string[]
};
const tcResult = await this.memoryService.taskCreate(createParams);
const result: ToolCallResult = {
name: 'task_create',
success: tcResult.success,
message: tcResult.message
};
return result;
}
case 'taskSetState': {
const setStateParams: TaskSetStateParams = {
taskId: args.taskId as string,
state: args.state as ToolStateArg
};
const tsResult = await this.memoryService.taskSetState(setStateParams);
const result: ToolCallResult = {
name: 'task_set_state',
success: tsResult.success,
message: tsResult.message
};
return result;
}
case 'taskDelete': {
const deleteParams: TaskDeleteParams = {
taskId: args.taskId as string,
deleteInfoNodes: (args.deleteInfoNodes as boolean) !== false
};
const tdResult = await this.memoryService.taskDelete(deleteParams);
const result: ToolCallResult = {
name: 'task_delete',
success: tdResult.success,
message: tdResult.message
};
return result;
}
case 'taskLinkInfo': {
const linkInfoParams: TaskLinkInfoParams = {
taskId: args.taskId as string,
infoNodeNames: args.infoNodeNames as string[]
};
const tliResult = await this.memoryService.taskLinkInfo(linkInfoParams);
const result: ToolCallResult = {
name: 'task_link_info',
success: tliResult.success,
message: tliResult.message
};
return result;
}
case 'taskArchive': {
const archiveParams: TaskArchiveParams = {
taskId: args.taskId as string,
summary: args.summary as string
};
const taResult = await this.memoryService.taskArchive(archiveParams);
const result: ToolCallResult = {
name: 'task_archive',
success: taResult.success,
message: taResult.message
};
return result;
}
case 'taskQuery': {
const tqResult = await this.memoryService.taskQuery({
limit: args.limit as number,
stateFilter: args.stateFilter as string
});
const result: ToolCallResult = {
name: 'task_query',
success: true,
message: `找到 ${tqResult.tasks.length} 个任务`
};
return result;
}
default: {
const defaultResult: ToolCallResult = {
name: name as string,
success: false,
message: '未实现的工具'
};
return defaultResult;
}
}
} catch (e) {
const errorMessage: string = (e as Error).message || '';
const errorResult: ToolCallResult = {
name: name as string,
success: false,
message: `执行失败: ${errorMessage}`
};
return errorResult;
}
}
/**
* 格式化人设数据为文本
*/
private formatPersona(persona: Record<string, string>): string {
const parts: string[] = [];
const keys: string[] = Object.keys(persona);
for (let i = 0; i < keys.length; i++) {
const key: string = keys[i];
const val: string = persona[key];
parts.push(`${key}: ${val}`);
}
return parts.length > 0 ? parts.join('') : '';
}
/**
* 构建上下文注入块
*/
private buildContextBlock(
personaResult: PersonaQueryResult,
taskResult: MemoryRecallResult
): string {
const blocks: string[] = [];
if (personaResult.found) {
blocks.push(`【当前人设】\n${this.formatPersona(personaResult.persona)}`);
}
if (taskResult.entities.length > 0) {
const entitySample: EntityInfo[] = taskResult.entities.slice(0, 5);
const entitiesStr: string = JSON.stringify(entitySample);
blocks.push(`【工作记忆】\n${taskResult.message}\n${entitiesStr}`);
}
return blocks.length > 0 ? blocks.join('\n\n') : '【新对话】';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +0,0 @@
export enum WidthBreakpoint {
WIDTH_XS = 'xs',
WIDTH_SM = 'sm',
WIDTH_MD = 'md',
WIDTH_LG = 'lg',
WIDTH_XL = 'xl'
}
export interface BreakpointTypes<T> {
xs?: T;
sm: T;
md: T;
lg: T;
xl?: T;
}
export class BreakpointType<T> {
private xs: T;
private sm: T;
private md: T;
private lg: T;
private xl: T;
public constructor(param: BreakpointTypes<T>) {
this.xs = param.xs ?? param.sm;
this.sm = param.sm;
this.md = param.md;
this.lg = param.lg;
this.xl = param.xl ?? param.lg;
}
public getValue(currentBreakpoint: WidthBreakpoint): T {
if (currentBreakpoint === WidthBreakpoint.WIDTH_XS) {
return this.xs;
}
if (currentBreakpoint === WidthBreakpoint.WIDTH_SM) {
return this.sm;
}
if (currentBreakpoint === WidthBreakpoint.WIDTH_MD) {
return this.md;
}
if (currentBreakpoint === WidthBreakpoint.WIDTH_XL) {
return this.xl;
}
return this.lg;
}
}

View File

@ -1,31 +0,0 @@
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,52 +0,0 @@
import { BreakpointType, WidthBreakpoint } from '../util/BreakpointSystem';
export interface VMEvent {
}
export class BaseViewModel {
protected isAttached: boolean = false;
protected isDisposed: boolean = false;
protected currentBreakpoint: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
attach(): void {
if (this.isAttached) {
return;
}
this.isAttached = true;
this.onAttach();
}
detach(): void {
if (!this.isAttached) {
return;
}
this.isAttached = false;
this.onDetach();
}
dispose(): void {
if (this.isDisposed) {
return;
}
this.isDisposed = true;
this.detach();
this.onDispose();
}
protected onAttach(): void {
}
protected onDetach(): void {
}
protected onDispose(): void {
}
public get attached(): boolean {
return this.isAttached;
}
public get disposed(): boolean {
return this.isDisposed;
}
}

View File

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

View File

@ -1 +0,0 @@
{"SECRET_KEY": "3a38a0f46a76673154b491a7b061c38f2f6a55489078ae7b5d34e94e75fc0534"}

View File

@ -1,17 +0,0 @@
/**
* 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 +0,0 @@
export { ChatPage } from './src/main/ets/pages/ChatPage';

View File

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

View File

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

View File

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

View File

@ -1,19 +0,0 @@
{
"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

@ -1,11 +0,0 @@
{
"name": "@ohos/chat",
"version": "1.0.0",
"description": "TrulyMEM chat feature module",
"main": "Index.ets",
"author": "",
"license": "",
"dependencies": {
"@ohos/common": "file:../../common"
}
}

View File

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

View File

@ -1,161 +0,0 @@
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,88 +0,0 @@
import { GraphDatabase, GraphMemoryService, AIAgentService, ChatMessage, AgentResponse, Logger } from '@ohos/common';
import { ChatMessageList, ChatInputBar } from '../components/ChatComponents';
@Component
export struct ChatPage {
@State messages: ChatMessage[] = [];
@State inputText: string = '';
@Prop db: GraphDatabase;
@State toolCallLog: string = '';
@State isThinking: boolean = false;
private agentService?: AIAgentService;
async aboutToAppear() {
// 初始化图记忆服务和 Agent
const memoryService = new GraphMemoryService(this.db);
this.agentService = new AIAgentService(memoryService, getContext(this));
// 加载历史消息(兼容旧数据:无 session_id 时加载全部)
const rawHistory = await this.db.getChatHistory(50, this.agentService.getSessionId());
if (rawHistory.length === 0) {
// 新 session尝试加载旧消息
const legacyHistory = await this.db.getChatHistory(50);
this.messages = legacyHistory.map(m => {
const msg: ChatMessage = { role: m.role, content: m.content };
return msg;
});
} else {
this.messages = rawHistory.map(m => {
const msg: ChatMessage = { role: m.role, content: m.content };
return msg;
});
}
}
async sendMessage() {
if (!this.inputText.trim() || !this.agentService) return;
const userMessage: string = this.inputText;
this.inputText = '';
// 添加用户消息
await this.db.saveChatMessage('user', userMessage, '', this.agentService.getSessionId());
this.messages = [...this.messages, { role: 'user', content: userMessage }];
// 显示 loading
this.isThinking = true;
this.toolCallLog = '';
try {
// 通过 Agent 发送消息
const agentResponse: AgentResponse = await this.agentService.sendMessage(userMessage);
// 记录工具调用日志
if (agentResponse.toolCalls.length > 0) {
const logs: string[] = agentResponse.toolCalls.map(tc => `🛠 ${tc.name}: ${tc.message}`);
this.toolCallLog = logs.join('\n');
}
// 保存并显示 AI 回复
await this.db.saveChatMessage('assistant', agentResponse.content, this.toolCallLog, this.agentService.getSessionId());
this.messages = [...this.messages, { role: 'assistant', content: agentResponse.content }];
} catch (err) {
Logger.error('Agent request failed: ' + JSON.stringify(err));
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${err.message || JSON.stringify(err)}` }];
} finally {
this.isThinking = false;
}
}
build() {
Column() {
ChatMessageList({
messages: this.messages,
isThinking: this.isThinking,
toolCallLog: this.toolCallLog
})
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,12 +0,0 @@
{
"module": {
"name": "chat",
"type": "har",
"description": "TrulyMEM chat feature module",
"deviceTypes": [
"phone",
"tablet",
"2in1"
]
}
}

View File

@ -1,17 +0,0 @@
/**
* 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 +0,0 @@
export { GraphPage } from './src/main/ets/pages/GraphPage';

View File

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

View File

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

View File

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

View File

@ -1,19 +0,0 @@
{
"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

@ -1,11 +0,0 @@
{
"name": "@ohos/graph",
"version": "1.0.0",
"description": "TrulyMEM graph feature module",
"main": "Index.ets",
"author": "",
"license": "",
"dependencies": {
"@ohos/common": "file:../../common"
}
}

View File

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

View File

@ -1,251 +0,0 @@
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,200 +0,0 @@
/**
* GraphPage — 记忆星图页面(重构后)
* 使用 WebView 显示 Three.js 3D 图可视化
* 子组件GraphWebView、GraphNodeSearchBar、NodeDetailPanel、GraphDataService、NativeBridge
*/
import web_webview from '@ohos.web.webview';
import {
GraphDatabase,
NodeDetailInfo,
Logger,
RecallEntity,
GraphMemoryService
} from '@ohos/common';
import {
GraphWebView,
GraphNodeSearchBar,
NodeDetailPanel,
GraphDataService,
NativeBridge
} from '../components/GraphComponents';
// ========= GraphPage 组件 =========
interface GraphNodeItem {
id: number;
label: string;
type: string;
mentions: number;
}
interface GraphEdgeItem {
id: number;
source: number;
target: number;
label: string;
relation: string;
}
@Component
export struct GraphPage {
private controller: web_webview.WebviewController = new web_webview.WebviewController();
@Prop db: GraphDatabase;
@State nodeCount: number = 0;
@State edgeCount: number = 0;
@State selectedNodeDetail: NodeDetailInfo | null = null;
@State showNodeDetail: boolean = false;
@State searchText: string = '';
private graphService: GraphMemoryService = new GraphMemoryService(this.db);
// 初始化桥接对象
private bridge: NativeBridge = new NativeBridge(
this.controller,
(): void => { this.pushGraphDataToWebView(); },
(nodeId: number, nodeName: string): void => { this.handleNodeClick(nodeId, nodeName); },
(query: string): void => { this.handleSearchFromWeb(query); }
);
/**
* 外部触发刷新图数据(聊天写入新记忆后调用)
*/
public async refreshGraphData(): Promise<void> {
await this.pushGraphDataToWebView();
}
/**
* 处理节点点击 - 查询详细信息并显示浮层
*/
private async handleNodeClick(nodeId: number, nodeName: string): Promise<void> {
try {
const detail: NodeDetailInfo | null = await this.graphService.getNodeDetail(nodeName);
if (detail) {
this.selectedNodeDetail = detail;
this.showNodeDetail = true;
}
} catch (err) {
Logger.error('handleNodeClick error: ' + JSON.stringify(err));
}
}
/**
* 处理来自 WebView 的搜索请求
*/
private handleSearchFromWeb(query: string): void {
this.searchText = query;
}
/**
* 处理搜索输入 - 通知 WebView 过滤
*/
private onSearchInput(value: string): void {
this.searchText = value;
const jsCode = `window.dispatchEvent(new MessageEvent('message', { data: { type: 'search_nodes', query: '${value}' } }));`;
this.controller.runJavaScript(jsCode);
}
/**
* 关闭节点详情浮层
*/
private closeNodeDetail(): void {
this.showNodeDetail = false;
this.selectedNodeDetail = null;
}
/**
* 从数据库读取全量图数据,推送给 WebView
*/
private async getAllNodesData(): 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
};
});
}
private async getAllEdgesData(): Promise<GraphEdgeItem[]> {
const recallResult = await this.db.recall('', [], 3);
const nameToId: Record<string, number> = {};
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
nameToId[e.name as string] = idx + 1;
});
const edgeItems: GraphEdgeItem[] = [];
for (let i = 0; i < recallResult.relations.length; i++) {
const r = recallResult.relations[i];
const sourceId: number | undefined = nameToId[r.source];
const targetId: number | undefined = nameToId[r.target];
if (sourceId !== undefined && targetId !== undefined) {
edgeItems.push({
id: i + 1,
source: sourceId,
target: targetId,
label: r.type,
relation: r.type
});
}
}
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 星图
Web({ src: $rawfile('graph.html'), controller: this.controller })
.javaScriptAccess(true)
.width('100%')
.height('100%')
.zoomAccess(true)
.onPageEnd(() => {
this.pushGraphDataToWebView();
})
.javaScriptProxy({
object: this.bridge,
name: 'nativeBridge',
methodList: ['onNodeClick', 'onSearch'],
asyncMethodList: ['requestGraphData'],
controller: this.controller
})
// 搜索框
GraphNodeSearchBar({
searchText: this.searchText,
onSearchInput: (value: string): void => { this.onSearchInput(value); }
})
// 节点详情浮层
if (this.showNodeDetail && this.selectedNodeDetail !== null) {
NodeDetailPanel({
detail: this.selectedNodeDetail,
onClose: (): void => { this.closeNodeDetail(); }
})
}
}
.width('100%')
.height('100%')
}
}

View File

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

View File

@ -1,827 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆星图 - TrulyMEM</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Courier New', monospace; background: #0a0a1a; color: #ffffff; overflow: hidden; width: 100vw; height: 100vh; }
#canvas-container { width: 100%; height: 100%; position: relative; }
canvas { display: block; }
#stats { position: absolute; top: 20px; left: 20px; background: rgba(10, 10, 26, 0.8); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; backdrop-filter: blur(10px); }
#stats h3 { margin-bottom: 8px; color: #4488ff; font-size: 16px; }
#stats p { margin: 4px 0; color: #aaaacc; }
#stats span { color: #ffffff; font-weight: bold; }
#node-info { position: absolute; top: 20px; right: 20px; background: rgba(10, 10, 26, 0.9); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; display: none; backdrop-filter: blur(10px); max-width: 300px; }
#node-info h3 { color: #44ff88; margin-bottom: 8px; font-size: 16px; }
#node-info p { margin: 4px 0; color: #aaaacc; }
#node-info .label { color: #8888aa; }
#loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; color: #4488ff; z-index: 200; }
#nav { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; z-index: 100; }
/* 搜索框 */
#search-box { position: absolute; top: 80px; left: 20px; z-index: 100; }
#search-input { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #ffffff; padding: 8px 12px; border-radius: 6px; font-family: 'Courier New', monospace; font-size: 14px; width: 200px; outline: none; backdrop-filter: blur(10px); }
#search-input::placeholder { color: #666688; }
/* 类型过滤按钮 */
#type-filter { position: absolute; top: 120px; left: 20px; display: flex; gap: 8px; z-index: 100; flex-wrap: wrap; }
.type-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 12px; backdrop-filter: blur(10px); transition: all 0.3s; }
.type-btn.active { background: rgba(68, 136, 255, 0.3); border-color: #4488ff; color: #ffffff; }
/* 缩放控制按钮 */
#zoom-controls { position: absolute; bottom: 100px; right: 20px; display: flex; flex-direction: column; gap: 10px; z-index: 100; }
.zoom-btn { width: 40px; height: 40px; border-radius: 50%; background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #ffffff; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(10px); font-family: 'Courier New', monospace; }
/* 边标签 */
#edge-label { position: absolute; display: none; background: rgba(10, 10, 26, 0.9); color: #44ff88; padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 150; border: 1px solid rgba(68, 255, 136, 0.3); }
.nav-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 14px; backdrop-filter: blur(10px); }
</style>
</head>
<body>
<div id="canvas-container">
<div id="loading">正在加载星图数据...</div>
<div id="stats">
<h3>🌌 记忆星图</h3>
<p>节点: <span id="node-count">0</span></p>
<p>边: <span id="edge-count">0</span></p>
<p>状态: <span id="status">初始化中...</span></p>
</div>
<div id="node-info">
<h3 id="info-name"></h3>
<p><span class="label">类型:</span> <span id="info-type"></span></p>
<p><span class="label">提及次数:</span> <span id="info-mentions"></span></p>
<p><span class="label">连接数:</span> <span id="info-links"></span></p>
</div>
<div id="nav">
<button class="nav-btn active">🌌 星图</button>
</div>
<div id="search-box">
<input type="text" id="search-input" placeholder="搜索节点...">
</div>
<div id="type-filter">
<button class="type-btn active" data-type="全部">全部</button>
<button class="type-btn" data-type="Person">Person</button>
<button class="type-btn" data-type="Task">Task</button>
<button class="type-btn" data-type="AI">AI</button>
<button class="type-btn" data-type="Concept">Concept</button>
<button class="type-btn" data-type="Object">Object</button>
</div>
<div id="zoom-controls">
<button class="zoom-btn" id="zoom-in">+</button>
<button class="zoom-btn" id="zoom-out">-</button>
<button class="zoom-btn" id="zoom-reset">R</button>
</div>
<div id="edge-label"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
let scene, camera, renderer, controls;
let nodes = [], edges = [];
let nodeMeshes = [], edgeLines = [];
let starField, nebulaParticles;
let raycaster, mouse;
let hoveredNode = null, selectedNode = null;
let animationId;
let highlightPulse = 0;
let searchTerm = '';
let activeTypeFilter = '全部';
let isDragging = false;
let dragNode = null;
let originalPhysicsState = true;
let edgeLabelEl = null;
let nodePositions = {}; // 存储节点位置用于拖拽
const typeColors = { 'person': 0x4488ff, 'task': 0xff8844, 'ai': 0xaa44ff, 'concept': 0x44ff88, 'object': 0xff4444 };
const defaultColor = 0xcccccc;
const edgeColors = { '喜欢': 0xff6b6b, '学习': 0x4ecdc4, '属于': 0x45b7d1, '相关': 0x96ceb4, '使用': 0xfeca57, '创建': 0xff9ff3 };
const defaultEdgeColor = 0x444466;
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 30, 60);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x0a0a1a, 1);
document.getElementById('canvas-container').appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
const ambientLight = new THREE.AmbientLight(0x444466, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 100, 50);
scene.add(directionalLight);
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
createStarField();
createNebula();
// 使用 ResizeObserver 监听容器尺寸变化(比 window.resize 更准确)
initResizeObserver();
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('click', onMouseClick);
window.addEventListener('message', (event) => {
if (event.data.type === 'graph_data') {
window.__graphData = event.data.payload;
loadGraphData();
}
if (event.data.type === 'search_nodes') {
searchTerm = event.data.query || '';
document.getElementById('search-input').value = searchTerm;
applyFilters();
}
if (event.data.type === 'node_detail') {
showNodeDetailPanel(event.data.detail);
}
if (event.data.type === 'highlight_node') {
highlightNodeById(event.data.nodeId);
}
});
// 搜索输入框事件
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', (e) => {
searchTerm = e.target.value.toLowerCase();
applyFilters();
// 通知 ArkTS
try {
if (window.nativeBridge && window.nativeBridge.onSearch) {
window.nativeBridge.onSearch(searchTerm);
}
} catch(e) {}
});
// 类型过滤按钮事件
document.querySelectorAll('.type-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeTypeFilter = btn.dataset.type;
applyFilters();
});
});
// 缩放控制按钮
document.getElementById('zoom-in').addEventListener('click', () => {
camera.position.multiplyScalar(0.8);
controls.update();
});
document.getElementById('zoom-out').addEventListener('click', () => {
camera.position.multiplyScalar(1.2);
controls.update();
});
document.getElementById('zoom-reset').addEventListener('click', () => {
if (Object.keys(nodePositions).length > 0) {
const allPositions = Object.values(nodePositions);
let maxDist = 0;
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
controls.target.set(0, 0, 0);
controls.update();
}
});
// 边标签元素
edgeLabelEl = document.getElementById('edge-label');
// 鼠标移动检测边悬停
renderer.domElement.addEventListener('mousemove', onEdgeHoverCheck);
// 通知 ArkTS 请求图数据
try {
if (window.nativeBridge && window.nativeBridge.requestGraphData) {
window.nativeBridge.requestGraphData();
}
} catch(e) {}
// 触摸事件支持
initTouchEvents();
// 初始化拖拽功能
initDragFunctionality();
animate();
}
function createStarField() {
const starCount = 3000;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const i3 = i * 3;
const radius = 400 + Math.random() * 600;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi);
const colorChoice = Math.random();
if (colorChoice < 0.7) {
colors[i3] = 0.8 + Math.random() * 0.2;
colors[i3 + 1] = 0.8 + Math.random() * 0.2;
colors[i3 + 2] = 1.0;
} else {
colors[i3] = 1.0;
colors[i3 + 1] = 0.9 + Math.random() * 0.1;
colors[i3 + 2] = 0.8 + Math.random() * 0.2;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true });
starField = new THREE.Points(geometry, material);
scene.add(starField);
}
function createNebula() {
const nebulaCount = 500;
const positions = new Float32Array(nebulaCount * 3);
const colors = new Float32Array(nebulaCount * 3);
for (let i = 0; i < nebulaCount; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 800;
positions[i3 + 1] = (Math.random() - 0.5) * 800;
positions[i3 + 2] = (Math.random() - 0.5) * 800;
const colorChoice = Math.random();
if (colorChoice < 0.33) {
colors[i3] = 0.5 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.7 + Math.random() * 0.3;
} else if (colorChoice < 0.66) {
colors[i3] = 0.2 + Math.random() * 0.2; colors[i3 + 1] = 0.3 + Math.random() * 0.3; colors[i3 + 2] = 0.8 + Math.random() * 0.2;
} else {
colors[i3] = 0.7 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.5 + Math.random() * 0.3;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending });
nebulaParticles = new THREE.Points(geometry, material);
scene.add(nebulaParticles);
}
window.loadGraphData = function(data) {
if (data && data.nodes && data.edges) {
nodes = data.nodes.map(n => ({ id: n.id, name: n.label || n.name, type: n.type, mention_count: n.mentions || 1 }));
edges = data.edges.map(e => ({ id: e.id, source: e.from || e.source, target: e.to || e.target, relation_type: e.label || e.relation }));
document.getElementById('node-count').textContent = nodes.length;
document.getElementById('edge-count').textContent = edges.length;
document.getElementById('status').textContent = '就绪';
createGraphVisualization();
document.getElementById('loading').style.display = 'none';
}
}
function createGraphVisualization() {
nodeMeshes.forEach(mesh => scene.remove(mesh));
edgeLines.forEach(line => scene.remove(line));
nodeMeshes = [];
edgeLines = [];
if (nodes.length === 0) return;
const nodeDegrees = {};
nodes.forEach(n => nodeDegrees[n.id] = 0);
edges.forEach(e => {
nodeDegrees[e.source] = (nodeDegrees[e.source] || 0) + 1;
nodeDegrees[e.target] = (nodeDegrees[e.target] || 0) + 1;
});
const positions = {};
nodePositions = positions; // 存储供拖拽使用
const maxDegree = Math.max(...Object.values(nodeDegrees), 1);
nodes.forEach((node, i) => {
const angle = (i / nodes.length) * Math.PI * 2;
const radius = 15 + (nodeDegrees[node.id] / maxDegree) * 35;
positions[node.id] = { x: radius * Math.cos(angle), y: (Math.random() - 0.5) * 10, z: radius * Math.sin(angle) };
});
for (let iter = 0; iter < 200; iter++) {
Object.keys(positions).forEach(id1 => {
Object.keys(positions).forEach(id2 => {
if (id1 >= id2) return;
const pos1 = positions[id1], pos2 = positions[id2];
const dx = pos1.x - pos2.x, dy = pos1.y - pos2.y, dz = pos1.z - pos2.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist < 20) { // 增加排斥距离
const force = 0.3 / (dist * dist); // 增加排斥力系数(平方反比)
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const dx = pos2.x - pos1.x, dy = pos2.y - pos1.y, dz = pos2.z - pos1.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist > 15) {
const force = 0.08; // 增加吸引力
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
}
nodes.forEach(node => {
const pos = positions[node.id];
if (!pos) return;
const radius = 0.4 + Math.min(node.mention_count * 0.15, 1.5);
const color = typeColors[node.type] || defaultColor;
const geometry = new THREE.SphereGeometry(radius, 16, 12);
const material = new THREE.MeshPhongMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 + Math.min(node.mention_count * 0.05, 0.3), shininess: 30, transparent: true, opacity: 0 });
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(pos.x, pos.y, pos.z);
sphere.userData = { nodeId: node.id, nodeData: node };
scene.add(sphere);
nodeMeshes.push(sphere);
// 淡入动画
fadeInObject(sphere, 500);
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const color = edgeColors[edge.relation_type] || defaultEdgeColor;
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(pos1.x, pos1.y, pos1.z), new THREE.Vector3(pos2.x, pos2.y, pos2.z)]);
const material = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0, linewidth: 1 });
const line = new THREE.Line(geometry, material);
line.userData = { edgeId: edge.id, edgeData: edge };
scene.add(line);
edgeLines.push(line);
// 淡入动画
fadeInLine(line, 500);
});
const allPositions = Object.values(positions);
if (allPositions.length > 0) {
let maxDist = 0;
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
controls.target.set(0, 0, 0);
}
}
function onMouseMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (hoveredNode !== node) {
if (hoveredNode) hoveredNode.scale.set(1, 1, 1);
hoveredNode = node;
node.scale.set(1.2, 1.2, 1.2);
showNodeInfo(node.userData.nodeData);
}
} else {
if (hoveredNode) { hoveredNode.scale.set(1, 1, 1); hoveredNode = null; hideNodeInfo(); }
}
}
function onMouseClick(event) {
if (isDragging) return;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (selectedNode === node) {
selectedNode = null;
document.getElementById('node-info').style.display = 'none';
resetHighlight();
} else {
selectedNode = node;
showNodeInfo(node.userData.nodeData, true);
highlightNodeConnections(node.userData.nodeData);
// 通知 ArkTS 节点被点击
try {
if (window.nativeBridge && window.nativeBridge.onNodeClick) {
window.nativeBridge.onNodeClick(node.userData.nodeData.id, node.userData.nodeData.name);
}
} catch(e) {}
}
} else {
// 点击空白处恢复
selectedNode = null;
document.getElementById('node-info').style.display = 'none';
resetHighlight();
}
}
function showNodeInfo(nodeData, isClick = false) {
document.getElementById('info-name').textContent = nodeData.name;
document.getElementById('info-type').textContent = nodeData.type;
document.getElementById('info-mentions').textContent = nodeData.mention_count;
const linkCount = edges.filter(e => e.source === nodeData.id || e.target === nodeData.id).length;
document.getElementById('info-links').textContent = linkCount;
if (isClick) document.getElementById('node-info').style.display = 'block';
}
function hideNodeInfo() { if (!selectedNode) document.getElementById('node-info').style.display = 'none'; }
// 淡入动画
function fadeInObject(obj, duration) {
const startOpacity = 0;
const endOpacity = obj.material.opacity !== undefined ? (obj.material.transparent ? obj.material.opacity : 1) : 1;
obj.material.opacity = startOpacity;
obj.material.transparent = true;
const startTime = Date.now();
function animateFade() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
obj.material.opacity = startOpacity + (endOpacity - startOpacity) * progress;
if (progress < 1) {
requestAnimationFrame(animateFade);
} else {
obj.material.transparent = endOpacity < 1;
}
}
animateFade();
}
function fadeInLine(line, duration) {
const startOpacity = 0;
const endOpacity = 0.4;
line.material.opacity = startOpacity;
const startTime = Date.now();
function animateFade() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
line.material.opacity = startOpacity + (endOpacity - startOpacity) * progress;
if (progress < 1) {
requestAnimationFrame(animateFade);
}
}
animateFade();
}
// 通过节点ID高亮节点供ArkTS调用
function highlightNodeById(nodeId) {
const mesh = nodeMeshes.find(m => m.userData.nodeData.id === nodeId);
if (mesh) {
selectedNode = mesh;
showNodeInfo(mesh.userData.nodeData, true);
highlightNodeConnections(mesh.userData.nodeData);
}
}
// 显示节点详情面板供ArkTS调用
function showNodeDetailPanel(detail) {
// 更新node-info面板显示详细信息
document.getElementById('info-name').textContent = detail.name;
document.getElementById('info-type').textContent = detail.type;
document.getElementById('info-mentions').textContent = detail.mention_count;
document.getElementById('info-links').textContent = detail.connection_count;
document.getElementById('node-info').style.display = 'block';
// 如果有连接信息,添加到面板
let detailHtml = `<h3>${detail.name}</h3>`;
detailHtml += `<p><span class="label">类型:</span> ${detail.type}</p>`;
detailHtml += `<p><span class="label">提及次数:</span> ${detail.mention_count}</p>`;
detailHtml += `<p><span class="label">连接数:</span> ${detail.connection_count}</p>`;
if (detail.connections && detail.connections.length > 0) {
detailHtml += `<p><span class="label">连接关系:</span></p><ul style="margin-left: 15px; font-size: 12px;">`;
detail.connections.forEach(conn => {
detailHtml += `<li>${conn.type}: ${conn.target_name}</li>`;
});
detailHtml += `</ul>`;
}
document.getElementById('node-info').innerHTML = detailHtml;
}
// 节点拖拽功能
function initDragFunctionality() {
let dragStartPos = { x: 0, y: 0 };
renderer.domElement.addEventListener('mousedown', (event) => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
isDragging = false;
dragNode = intersects[0].object;
dragStartPos = { x: event.clientX, y: event.clientY };
originalPhysicsState = true; // 暂停物理模拟
}
});
renderer.domElement.addEventListener('mousemove', (event) => {
if (dragNode) {
const dx = event.clientX - dragStartPos.x;
const dy = event.clientY - dragStartPos.y;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
isDragging = true;
}
if (isDragging) {
// 将屏幕坐标转换为3D空间
const rect = renderer.domElement.getBoundingClientRect();
const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
const vector = new THREE.Vector3(mouseX, mouseY, 0.5);
vector.unproject(camera);
const dir = vector.sub(camera.position).normalize();
const distance = -camera.position.z / dir.z;
const newPos = camera.position.clone().add(dir.multiplyScalar(distance));
dragNode.position.copy(newPos);
// 更新存储的位置
if (nodePositions[dragNode.userData.nodeId]) {
nodePositions[dragNode.userData.nodeId] = { x: newPos.x, y: newPos.y, z: newPos.z };
}
}
}
});
renderer.domElement.addEventListener('mouseup', () => {
if (dragNode) {
// 恢复物理模拟
dragNode = null;
isDragging = false;
}
});
}
// Touch事件支持
function initTouchEvents() {
let touchStart = null;
let touchStartDistance = 0;
let touchStartPos = { x: 0, y: 0 };
let isTouchDrag = false;
renderer.domElement.addEventListener('touchstart', (event) => {
event.preventDefault();
if (event.touches.length === 1) {
const touch = event.touches[0];
touchStartPos = { x: touch.clientX, y: touch.clientY };
isTouchDrag = false;
// 模拟鼠标事件用于射线检测
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (selectedNode === node) {
selectedNode = null;
document.getElementById('node-info').style.display = 'none';
resetHighlight();
} else {
selectedNode = node;
showNodeInfo(node.userData.nodeData, true);
highlightNodeConnections(node.userData.nodeData);
try {
if (window.nativeBridge && window.nativeBridge.onNodeClick) {
window.nativeBridge.onNodeClick(node.userData.nodeData.id, node.userData.nodeData.name);
}
} catch(e) {}
}
}
} else if (event.touches.length === 2) {
// 双指缩放
const dx = event.touches[0].clientX - event.touches[1].clientX;
const dy = event.touches[0].clientY - event.touches[1].clientY;
touchStartDistance = Math.sqrt(dx*dx + dy*dy);
}
}, { passive: false });
renderer.domElement.addEventListener('touchmove', (event) => {
event.preventDefault();
if (event.touches.length === 1 && controls) {
const touch = event.touches[0];
const dx = touch.clientX - touchStartPos.x;
const dy = touch.clientY - touchStartPos.y;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
isTouchDrag = true;
}
// 模拟OrbitControls的鼠标移动
if (isTouchDrag) {
const rotateSpeed = 0.005;
controls.rotateLeft(-dx * rotateSpeed);
controls.rotateUp(-dy * rotateSpeed);
controls.update();
touchStartPos = { x: touch.clientX, y: touch.clientY };
}
} else if (event.touches.length === 2 && controls) {
// 双指缩放
const dx = event.touches[0].clientX - event.touches[1].clientX;
const dy = event.touches[0].clientY - event.touches[1].clientY;
const distance = Math.sqrt(dx*dx + dy*dy);
const scale = touchStartDistance / distance;
camera.position.multiplyScalar(scale);
controls.update();
touchStartDistance = distance;
}
}, { passive: false });
renderer.domElement.addEventListener('touchend', (event) => {
if (event.touches.length === 0) {
isTouchDrag = false;
}
}, { passive: false });
}
// 搜索和类型过滤
function applyFilters() {
nodeMeshes.forEach(mesh => {
const nodeData = mesh.userData.nodeData;
const nameMatch = nodeData.name.toLowerCase().includes(searchTerm);
const typeMatch = activeTypeFilter === '全部' || nodeData.type === activeTypeFilter;
if (nameMatch && typeMatch) {
mesh.material.transparent = false;
mesh.material.opacity = 1;
mesh.scale.setScalar(1);
} else {
mesh.material.transparent = true;
mesh.material.opacity = 0.2;
mesh.scale.setScalar(0.8);
}
});
// 同时过滤边
edgeLines.forEach(line => {
const edgeData = line.userData.edgeData;
const sourceNode = nodes.find(n => n.id === edgeData.source);
const targetNode = nodes.find(n => n.id === edgeData.target);
const sourceMatch = sourceNode && sourceNode.name.toLowerCase().includes(searchTerm) && (activeTypeFilter === '全部' || sourceNode.type === activeTypeFilter);
const targetMatch = targetNode && targetNode.name.toLowerCase().includes(searchTerm) && (activeTypeFilter === '全部' || targetNode.type === activeTypeFilter);
if ((sourceMatch || targetMatch) && searchTerm === '' && activeTypeFilter === '全部') {
line.material.transparent = true;
line.material.opacity = 0.4;
} else if (sourceMatch || targetMatch) {
line.material.transparent = true;
line.material.opacity = 0.6;
} else {
line.material.transparent = true;
line.material.opacity = 0.1;
}
});
}
// 连接高亮
function highlightNodeConnections(nodeData) {
const connectedNodeIds = new Set();
const connectedEdgeIds = new Set();
// 找出所有连接的节点和边
edges.forEach(edge => {
if (edge.source === nodeData.id || edge.target === nodeData.id) {
connectedNodeIds.add(edge.source);
connectedNodeIds.add(edge.target);
connectedEdgeIds.add(edge.id);
}
});
nodeMeshes.forEach(mesh => {
const meshNodeId = mesh.userData.nodeData.id;
if (meshNodeId === nodeData.id) {
// 选中的节点
mesh.material.emissiveIntensity = 1.0;
mesh.scale.setScalar(1.5);
} else if (connectedNodeIds.has(meshNodeId)) {
// 直接连接的节点
mesh.material.emissiveIntensity = 0.8;
mesh.scale.setScalar(1.3);
mesh.material.transparent = false;
mesh.material.opacity = 1;
} else {
// 无关系的节点
mesh.material.transparent = true;
mesh.material.opacity = 0.15;
mesh.material.emissiveIntensity = 0.2;
mesh.scale.setScalar(0.9);
}
});
edgeLines.forEach(line => {
if (connectedEdgeIds.has(line.userData.edgeData.id)) {
line.material.transparent = true;
line.material.opacity = 0.8;
line.material.linewidth = 2;
} else {
line.material.transparent = true;
line.material.opacity = 0.1;
line.material.linewidth = 1;
}
});
}
function resetHighlight() {
nodeMeshes.forEach(mesh => {
const nodeData = mesh.userData.nodeData;
const nameMatch = nodeData.name.toLowerCase().includes(searchTerm);
const typeMatch = activeTypeFilter === '全部' || nodeData.type === activeTypeFilter;
mesh.material.emissiveIntensity = 0.5 + Math.min(nodeData.mention_count * 0.05, 0.3);
if (nameMatch && typeMatch) {
mesh.material.transparent = false;
mesh.material.opacity = 1;
mesh.scale.setScalar(1);
} else {
mesh.material.transparent = true;
mesh.material.opacity = 0.2;
mesh.scale.setScalar(0.8);
}
});
edgeLines.forEach(line => {
line.material.transparent = true;
line.material.opacity = 0.4;
line.material.linewidth = 1;
});
}
// 边标签显示
function onEdgeHoverCheck(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 检查边悬停
const edgeIntersects = raycaster.intersectObjects(edgeLines);
if (edgeIntersects.length > 0) {
const edge = edgeIntersects[0].object;
const edgeData = edge.userData.edgeData;
edgeLabelEl.textContent = edgeData.relation_type || '关系';
edgeLabelEl.style.display = 'block';
edgeLabelEl.style.left = (event.clientX - rect.left + 10) + 'px';
edgeLabelEl.style.top = (event.clientY - rect.top - 10) + 'px';
} else {
edgeLabelEl.style.display = 'none';
}
}
// ========= ResizeObserver 响应式适配 =========
let containerObserver = null;
function initResizeObserver() {
const container = document.getElementById('canvas-container');
if (!container) return;
containerObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
onContainerResize(width, height);
}
}
});
containerObserver.observe(container);
}
function onContainerResize(width, height) {
if (!camera || !renderer) return;
const aspect = width / height;
camera.aspect = aspect;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
// 窄屏(<500px)时自动调整UI元素尺寸和位置
const isNarrow = width < 500;
const stats = document.getElementById('stats');
const nodeInfo = document.getElementById('node-info');
const searchBox = document.getElementById('search-box');
const typeFilter = document.getElementById('type-filter');
const searchInput = document.getElementById('search-input');
if (isNarrow) {
if (stats) {
stats.style.fontSize = '11px';
stats.style.padding = '8px 12px';
stats.style.top = '6px';
stats.style.left = '6px';
}
if (nodeInfo) {
nodeInfo.style.fontSize = '11px';
nodeInfo.style.padding = '8px 12px';
nodeInfo.style.maxWidth = '180px';
nodeInfo.style.top = '6px';
nodeInfo.style.right = '6px';
}
if (searchBox) { searchBox.style.top = '70px'; searchBox.style.left = '6px'; }
if (searchInput) { searchInput.style.width = '140px'; searchInput.style.fontSize = '12px'; }
if (typeFilter) { typeFilter.style.top = '108px'; typeFilter.style.left = '6px'; }
} else {
if (stats) {
stats.style.fontSize = '14px';
stats.style.padding = '15px 20px';
stats.style.top = '20px';
stats.style.left = '20px';
}
if (nodeInfo) {
nodeInfo.style.fontSize = '14px';
nodeInfo.style.padding = '15px 20px';
nodeInfo.style.maxWidth = '300px';
nodeInfo.style.top = '20px';
nodeInfo.style.right = '20px';
}
if (searchBox) { searchBox.style.top = '80px'; searchBox.style.left = '20px'; }
if (searchInput) { searchInput.style.width = '200px'; searchInput.style.fontSize = '14px'; }
if (typeFilter) { typeFilter.style.top = '120px'; typeFilter.style.left = '20px'; }
}
}
function animate() {
animationId = requestAnimationFrame(animate);
const time = Date.now() * 0.001;
highlightPulse = (highlightPulse + 0.02) % (Math.PI * 2);
controls.update();
if (starField) starField.rotation.y += 0.0001;
if (hoveredNode) { const pulse = 1 + Math.sin(highlightPulse * 3) * 0.05; hoveredNode.scale.set(pulse * 1.2, pulse * 1.2, pulse * 1.2); }
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>

View File

@ -1,17 +0,0 @@
/**
* 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 +0,0 @@
export { SettingsPage } from './src/main/ets/pages/SettingsPage';

View File

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

View File

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

View File

@ -1,19 +0,0 @@
{
"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

@ -1,11 +0,0 @@
{
"name": "@ohos/settings",
"version": "1.0.0",
"description": "TrulyMEM settings feature module",
"main": "Index.ets",
"author": "",
"license": "",
"dependencies": {
"@ohos/common": "file:../../common"
}
}

View File

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

View File

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

View File

@ -1,105 +0,0 @@
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,60 +0,0 @@
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 store: AppConfigStore = new AppConfigStore();
async aboutToAppear() {
const ctx = getContext(this);
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', '');
}
private async saveConfig(key: string, value: string): Promise<void> {
await this.store.setString(key, value);
}
build() {
Stack() {
GlowBackground()
Column() {
SettingsSectionHeader({ title: 'API 配置' })
SettingInputItem({
label: 'Base URL',
placeholder: 'https://api.deepseek.com',
value: this.baseUrl,
onValueChange: (v: string): void => { this.saveConfig('base_url', v); }
})
SettingInputItem({
label: 'Model ID',
placeholder: 'deepseek-chat',
value: this.model,
onValueChange: (v: string): void => { this.saveConfig('model', v); }
})
SettingInputItem({
label: 'API Key',
placeholder: 'sk-...',
value: this.apiKey,
isPassword: true,
onValueChange: (v: string): void => { this.saveConfig('api_key', v); }
})
}
.padding(16)
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('rgba(26,27,46,0.95)')
.backgroundBlurStyle(BlurStyle.Regular)
}
}

View File

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

View File

@ -1,221 +0,0 @@
Reading package lists...
Building dependency tree...
Reading state information...
python3 is already the newest version (3.13.9-3).
python3 set to manually installed.
python3-pip is already the newest version (26.0.1+dfsg-1).
You might want to run 'apt --fix-broken install' to correct these.
The following packages have unmet dependencies:
libxfont2 : Depends: libfontenc1 (>= 1:1.1.8) but 1:1.1.4-1 is to be installed
python3-venv : Depends: python3.13-venv (>= 3.13.5-1~) but it is not going to be installed
Depends: python3 (= 3.13.5-1) but 3.13.9-3 is to be installed
xserver-common : Depends: x11-xkb-utils but it is not going to be installed
Recommends: xfonts-base but it is not going to be installed
E: Unmet dependencies. Try 'apt --fix-broken install' with no packages (or specify a solution).
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
Reading package lists...
Building dependency tree...
Reading state information...
Correcting dependencies... Done
Solving dependencies...
Upgrading:
libfontenc1 libxt-dev
Installing dependencies:
libxt6t64 x11-xkb-utils
REMOVING:
libxt6
apt-listchanges: Reading changelogs...
dpkg-preconfigure: unable to re-open stdin: No such file or directory
Summary:
Upgrading: 2, Installing: 2, Removing: 1, Not Upgrading: 708
Download size: 0 B / 778 kB
Space needed: 526 kB / 419 GB available
(Reading database…
(Reading database… 5%
(Reading database… 10%
(Reading database… 15%
(Reading database… 20%
(Reading database… 25%
(Reading database… 30%
(Reading database… 35%
(Reading database… 40%
(Reading database… 45%
(Reading database… 50%
(Reading database… 55%
(Reading database… 60%
(Reading database… 65%
(Reading database… 70%
(Reading database… 75%
(Reading database… 80%
(Reading database… 85%
(Reading database… 90%
(Reading database… 95%
(Reading database… 100%
(Reading database… 103500 files and directories currently installed.)
Preparing to unpack …/libxt-dev_1%3a1.2.1-1.2+b2_amd64.deb…
Unpacking libxt-dev:amd64 (1:1.2.1-1.2+b2) over (1:1.2.1-1.1)…
dpkg: libxt6:amd64: dependency problems, but removing anyway as you requested:
x11-xserver-utils depends on libxt6.
x11-utils depends on libxt6 (>= 1:1.1.0).
libxmu6:amd64 depends on libxt6.
libxaw7:amd64 depends on libxt6.
libgs10:amd64 depends on libxt6.
(Reading database…
(Reading database… 5%
(Reading database… 10%
(Reading database… 15%
(Reading database… 20%
(Reading database… 25%
(Reading database… 30%
(Reading database… 35%
(Reading database… 40%
(Reading database… 45%
(Reading database… 50%
(Reading database… 55%
(Reading database… 60%
(Reading database… 65%
(Reading database… 70%
(Reading database… 75%
(Reading database… 80%
(Reading database… 85%
(Reading database… 90%
(Reading database… 95%
(Reading database… 100%
(Reading database… 103501 files and directories currently installed.)
Removing libxt6:amd64 (1:1.2.1-1.1)…
Selecting previously unselected package libxt6t64:amd64.
(Reading database…
(Reading database… 5%
(Reading database… 10%
(Reading database… 15%
(Reading database… 20%
(Reading database… 25%
(Reading database… 30%
(Reading database… 35%
(Reading database… 40%
(Reading database… 45%
(Reading database… 50%
(Reading database… 55%
(Reading database… 60%
(Reading database… 65%
(Reading database… 70%
(Reading database… 75%
(Reading database… 80%
(Reading database… 85%
(Reading database… 90%
(Reading database… 95%
(Reading database… 100%
(Reading database… 103495 files and directories currently installed.)
Preparing to unpack …/libxt6t64_1%3a1.2.1-1.2+b2_amd64.deb…
Unpacking libxt6t64:amd64 (1:1.2.1-1.2+b2)…
Preparing to unpack …/libfontenc1_1%3a1.1.8-1+b2_amd64.deb…
Unpacking libfontenc1:amd64 (1:1.1.8-1+b2) over (1:1.1.4-1)…
Selecting previously unselected package x11-xkb-utils.
Preparing to unpack …/x11-xkb-utils_7.7+9_amd64.deb…
Unpacking x11-xkb-utils (7.7+9)…
Setting up libfontenc1:amd64 (1:1.1.8-1+b2)…
Setting up libxt6t64:amd64 (1:1.2.1-1.2+b2)…
Setting up x11-xkb-utils (7.7+9)…
Setting up libxt-dev:amd64 (1:1.2.1-1.2+b2)…
Processing triggers for man-db (2.11.2-2)…
Processing triggers for libc-bin (2.42-14)…
needrestart is being skipped since dpkg has failed
Reading package lists...
Building dependency tree...
Reading state information...
Solving dependencies...
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:
The following packages have unmet dependencies:
python3-venv : Depends: python3.13-venv (>= 3.13.5-1~) but it is not going to be installed
Depends: python3 (= 3.13.5-1) but 3.13.9-3 is to be installed
E: Unable to satisfy dependencies. Reached two conflicting assignments:
1. python3-venv:amd64=3.13.5-1 is selected for install
2. python3-venv:amd64=3.13.5-1 Depends python3 (= 3.13.5-1)
but none of the choices are installable:
- python3:amd64=3.13.5-1 is not selected for install
===== Building TrulyMEM for Linux =====
Project root: /home/program/TrulyMEM-TrueHumanMEM
The virtual environment was not created successfully because ensurepip is not
available. On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.
apt install python3.13-venv
You may need to use sudo with that command. After installing the python3-venv
package, recreate your virtual environment.
Failing command: /home/program/TrulyMEM-TrueHumanMEM/.venv_build/bin/python3
Warning: venv creation failed, falling back to system Python
Cleaning previous builds...
================================
Building TrulyMEM (TUI + Web embedded)
================================
31 INFO: PyInstaller: 6.20.0, contrib hooks: 2026.4
31 INFO: Python: 3.13.12
33 INFO: Platform: Linux-6.1.0-44-amd64-x86_64-with-glibc2.42
33 INFO: Python environment: /usr
36 INFO: Removing temporary files and cleaning cache in /root/.cache/pyinstaller
37 INFO: Module search paths (PYTHONPATH):
['/home/program/TrulyMEM-TrueHumanMEM',
'/home/program/TrulyMEM-TrueHumanMEM',
'/usr/lib/python313.zip',
'/usr/lib/python3.13',
'/usr/lib/python3.13/lib-dynload',
'/usr/local/lib/python3.13/dist-packages',
'/usr/lib/python3/dist-packages',
'/home/program/TrulyMEM-TrueHumanMEM']
158 INFO: Appending 'datas' from .spec
158 INFO: checking Analysis
158 INFO: Building Analysis because Analysis-00.toc is non existent
159 INFO: Looking for Python shared library...
166 INFO: Using Python shared library: /usr/lib/x86_64-linux-gnu/libpython3.13.so.1.0
166 INFO: Running Analysis Analysis-00.toc
166 INFO: Target bytecode optimization level: 0
166 INFO: Initializing module dependency graph...
166 INFO: Initializing module graph hook caches...
170 INFO: Analyzing modules for base_library.zip ...
651 INFO: Processing standard module hook 'hook-encodings.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
943 INFO: Processing standard module hook 'hook-heapq.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
1656 INFO: Processing standard module hook 'hook-pickle.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2496 INFO: Caching module dependency graph...
2519 INFO: Analyzing /home/program/TrulyMEM-TrueHumanMEM/trulymem_entry.py
2554 INFO: Processing standard module hook 'hook-sqlite3.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2678 INFO: Processing standard module hook 'hook-platform.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2702 INFO: Processing standard module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2706 INFO: Processing standard module hook 'hook-_ctypes.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
2716 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
2717 INFO: SetuptoolsInfo: initializing cached setuptools info...
4768 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
4924 INFO: Processing standard module hook 'hook-xml.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
5338 INFO: Processing standard module hook 'hook-pydantic.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
5622 INFO: Processing standard module hook 'hook-rich.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
5908 INFO: Processing standard module hook 'hook-pygments.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
6310 INFO: Processing standard module hook 'hook-chardet.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
7729 INFO: Processing standard module hook 'hook-zoneinfo.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
8919 INFO: Processing standard module hook 'hook-certifi.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
8990 INFO: Processing standard module hook 'hook-anyio.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
9647 INFO: Processing standard module hook 'hook-difflib.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
10808 INFO: Processing standard module hook 'hook-numpy.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
12163 INFO: Processing standard module hook 'hook-charset_normalizer.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
12999 INFO: Processing standard module hook 'hook-pytz.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
13591 INFO: Processing pre-safe-import-module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
13598 INFO: Processing standard module hook 'hook-platformdirs.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
15377 INFO: Processing standard module hook 'hook-jinja2.py' from '/usr/local/lib/python3.13/dist-packages/_pyinstaller_hooks_contrib/stdhooks'
15747 INFO: Processing pre-safe-import-module hook 'hook-importlib_metadata.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15747 INFO: Setuptools: 'importlib_metadata' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.importlib_metadata'!
15752 INFO: Processing standard module hook 'hook-setuptools.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks'
15759 INFO: Processing pre-safe-import-module hook 'hook-distutils.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'
15778 INFO: Processing pre-safe-import-module hook 'hook-jaraco.py' from '/usr/local/lib/python3.13/dist-packages/PyInstaller/hooks/pre_safe_import_module'

View File

@ -1,25 +0,0 @@
#!/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"

View File

@ -1,23 +0,0 @@
{
"modelVersion": "5.0.5",
"dependencies": {
},
"execution": {
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
// "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
// "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
// "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
// "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
// "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
},
"logging": {
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
},
"debugging": {
// "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
},
"nodeOptions": {
// "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
// "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
}
}

View File

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

View File

@ -1,2 +0,0 @@
hwsdk.dir=/home/program/tools/command-line-tools/sdk/default
sdk.dir=/home/program/tools/command-line-tools/sdk/default

View File

@ -1,28 +0,0 @@
{
"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,9 +0,0 @@
{
"modelVersion": "5.0.5",
"description": "TrulyMEM - True Human Memory",
"dependencies": {},
"devDependencies": {
"@ohos/hypium": "1.0.24",
"@ohos/hamock": "1.0.0"
}
}

View File

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

View File

@ -1,177 +0,0 @@
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

@ -1,82 +0,0 @@
# 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

@ -1,25 +0,0 @@
/*
* 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

@ -1,17 +0,0 @@
/*
* 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

@ -1,17 +0,0 @@
/*
* 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

@ -1,58 +0,0 @@
/*
* 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

@ -1,17 +0,0 @@
/*
* 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

@ -1,16 +0,0 @@
/*
* 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

@ -1,17 +0,0 @@
/*
* 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

@ -1,28 +0,0 @@
/*
* 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

@ -1,97 +0,0 @@
/*
* 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

@ -1,118 +0,0 @@
/*
* 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

@ -1,48 +0,0 @@
/*
* 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

@ -1,63 +0,0 @@
/*
* 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

@ -1,253 +0,0 @@
/*
* 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

@ -1,294 +0,0 @@
/*
* 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

@ -1,45 +0,0 @@
/*
* 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

@ -1,56 +0,0 @@
/*
* 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

@ -1,22 +0,0 @@
{
"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

@ -1,25 +0,0 @@
{
"$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

@ -1,17 +0,0 @@
/**
* 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

@ -1,33 +0,0 @@
### 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

@ -1,177 +0,0 @@
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

@ -1,229 +0,0 @@
<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,31 +0,0 @@
{
"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

@ -1,6 +0,0 @@
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}

View File

@ -1,150 +0,0 @@
/*
* 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

@ -1,137 +0,0 @@
/*
* 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

@ -1,261 +0,0 @@
/*
* 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
};

Some files were not shown because too many files have changed in this diff Show More