Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bd14a5868 | |||
| c33e7469ac | |||
| 697a639cbe | |||
| 33a2b8c749 | |||
| c930b20e84 | |||
| 9e067ac23a | |||
| 7314d85ac6 | |||
| a4a904f66b | |||
| 135041e8b0 | |||
| 3d58ff7766 | |||
| 192179a986 | |||
| 85c3431b52 | |||
| 4caf115ec9 | |||
| e8c275c8f8 | |||
| 931617624e | |||
| ee2fd18fec | |||
| aa18c1c8b1 | |||
| d05bb5507f | |||
| 904661d73f | |||
| 43172e257a |
85
.gitignore
vendored
85
.gitignore
vendored
@ -1,19 +1,76 @@
|
||||
# Build cache
|
||||
.hvigor/
|
||||
entry/build/default/
|
||||
trulymem-core/build/default/
|
||||
trulymem-core/.preview/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Python cache
|
||||
# Python
|
||||
__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
|
||||
graph_memory.db
|
||||
# Virtual Environment
|
||||
venv/
|
||||
test_venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.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
|
||||
|
||||
# Task Archive (runtime generated)
|
||||
task_archive/
|
||||
ts/task_archive/
|
||||
|
||||
29
.sisyphus/fix-tracker.md
Normal file
29
.sisyphus/fix-tracker.md
Normal file
@ -0,0 +1,29 @@
|
||||
# 修复追踪文件
|
||||
|
||||
**分支**: openclaw
|
||||
**开始时间**: 2026-04-16
|
||||
**状态**: 重新执行
|
||||
|
||||
---
|
||||
|
||||
## 修复计划
|
||||
|
||||
### P0 - 阻断性问题
|
||||
|
||||
#### 1. 删除自定义 tool_interface.ts
|
||||
#### 2. 重构 graph_memory_tool.ts
|
||||
#### 3. 更新 plugin-entry.ts
|
||||
|
||||
### P2 - 最佳实践
|
||||
|
||||
#### 4. 重组 Skill 目录结构
|
||||
#### 5. 更新 bundled-skills 目录结构
|
||||
#### 6. 更新 openclaw.plugin.json
|
||||
#### 7. 更新 README.md / README_EN.md
|
||||
|
||||
---
|
||||
|
||||
## 执行记录
|
||||
|
||||
### [进行中] P0: 核心修复
|
||||
|
||||
117
.sisyphus/plans/migration_plan.md
Normal file
117
.sisyphus/plans/migration_plan.md
Normal file
@ -0,0 +1,117 @@
|
||||
# TrulyMEM → WaterFlow 迁移计划
|
||||
|
||||
> ⚠️ **修改只在 TrulyMEM 的 waterflow 分支执行** ⚠️
|
||||
>
|
||||
> 所有代码修改仅应用于 TrulyMEM 仓库的 `waterflow` 分支,作为 WaterFlow 框架的适配版本。
|
||||
|
||||
---
|
||||
|
||||
## 迁移目标
|
||||
|
||||
将 TrulyMEM 的图记忆能力从 Python 迁移到 TypeScript,适配 WaterFlow 框架。
|
||||
|
||||
**代码位置**: `/home/program/TrulyMEM-TrueHumanMEM/` (waterflow 分支)
|
||||
|
||||
---
|
||||
|
||||
## 迁移策略
|
||||
|
||||
将图记忆能力作为 TypeScript 模块添加到 waterflow 分支:
|
||||
|
||||
| TrulyMEM (Python) | WaterFlow (TypeScript) |
|
||||
|-------------------|------------------------|
|
||||
| `EmbeddedGraphDB` | `GraphDatabase` |
|
||||
| `GraphMemoryClient` | `MemoryService` |
|
||||
| 12 个记忆工具 | `GraphMemoryTool` + Skills |
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Phase 1: 项目结构
|
||||
|
||||
- [x] 1.1 创建 `ts/` 目录 - TypeScript 项目
|
||||
- [x] 1.2 创建 `package.json` - 项目配置
|
||||
- [x] 1.3 创建 `tsconfig.json` - TypeScript 配置
|
||||
|
||||
### Phase 2: 核心库
|
||||
|
||||
- [x] 2.1 创建 `ts/src/runtime/core/graph_memory/types.ts` - 类型定义
|
||||
- [x] 2.2 创建 `ts/src/runtime/core/graph_memory/graph_database.ts` - 图数据库
|
||||
- [x] 2.3 创建 `ts/src/runtime/core/graph_memory/memory_service.ts` - 记忆服务
|
||||
- [x] 2.4 创建 `ts/src/runtime/core/graph_memory/index.ts` - 模块导出
|
||||
|
||||
### Phase 3: Tool 接口
|
||||
|
||||
- [x] 3.1 创建 `ts/src/runtime/core/tools/builtin/graph_memory_tool.ts` - Tool 实现
|
||||
- [x] 3.2 注册 Tool (作为独立模块导出)
|
||||
|
||||
### Phase 4: Skill 定义
|
||||
|
||||
- [x] 4.1 创建 `ts/bundled-skills/graph_memory/SKILL.md` - 主 Skill
|
||||
- [x] 4.2 创建 `ts/bundled-skills/graph_memory/persona/SKILL.md` - Persona
|
||||
- [x] 4.3 创建 `ts/bundled-skills/graph_memory/task/SKILL.md` - 任务管理
|
||||
|
||||
### Phase 5: 验证
|
||||
|
||||
- [x] 5.1 编译 TypeScript - 无错误
|
||||
- [ ] 5.2 运行测试
|
||||
|
||||
---
|
||||
|
||||
## 目录结构 (在 TrulyMEM waterflow 分支)
|
||||
|
||||
```
|
||||
TrulyMEM-TrueHumanMEM/
|
||||
├── ts/ # TypeScript 项目 (保留)
|
||||
│ ├── src/
|
||||
│ │ └── runtime/core/
|
||||
│ │ ├── graph_memory/ # 图记忆模块
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── types.ts
|
||||
│ │ │ ├── graph_database.ts
|
||||
│ │ │ └── memory_service.ts
|
||||
│ │ └── tools/
|
||||
│ │ └── builtin/
|
||||
│ │ └── graph_memory_tool.ts
|
||||
│ ├── bundled-skills/
|
||||
│ │ └── graph_memory/
|
||||
│ │ ├── SKILL.md
|
||||
│ │ ├── persona/SKILL.md
|
||||
│ │ └── task/SKILL.md
|
||||
│ ├── package.json
|
||||
│ └── tsconfig.json
|
||||
│
|
||||
├── docs/integration/waterflow-design.md # 迁移设计文档 (保留)
|
||||
│
|
||||
└── (其他文件迁移后删除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 迁移后清理
|
||||
|
||||
迁移完成后,waterflow 分支将删除以下文件:
|
||||
|
||||
- `core/` - Python 核心代码
|
||||
- `ui/` - Python UI 代码
|
||||
- `tests/` - Python 测试
|
||||
- `tools/` - Python 工具
|
||||
- `trulymem_entry.py` - Python 入口
|
||||
- `build/` - 构建脚本
|
||||
- `pic/` - 图片资源 (除图标外)
|
||||
- `requirements.txt` - Python 依赖
|
||||
- `TrulyMEM.spec` - Python 打包配置
|
||||
|
||||
只保留:
|
||||
- `ts/` - TypeScript 源码
|
||||
- `docs/integration/waterflow-design.md` - 迁移文档
|
||||
- `.gitignore`, `LICENSE`
|
||||
|
||||
---
|
||||
|
||||
## 工作追踪
|
||||
|
||||
工作进度记录在: `todo_progress.md`
|
||||
|
||||
每次修改文件前后请查看此文件并更新进度。
|
||||
572
.sisyphus/plans/plan.md
Normal file
572
.sisyphus/plans/plan.md
Normal file
@ -0,0 +1,572 @@
|
||||
# TrulyMEM → OpenClaw 重构修复计划
|
||||
|
||||
> **分支**: `openclaw`
|
||||
> **代码位置**: `/home/program/TrulyMEM-TrueHumanMEM/`
|
||||
> **目标**: 将当前 TypeScript 实现完全适配 OpenClaw 框架的接口规范,并补全 main 分支缺失的功能
|
||||
|
||||
---
|
||||
|
||||
## 背景分析
|
||||
|
||||
### 当前状态
|
||||
|
||||
openclaw 分支已完成骨架迁移:类型定义、核心类(GraphDatabase/MemoryService/GraphMemoryTool)、Skill 定义文件均已就位,TypeScript 编译通过。
|
||||
|
||||
### 核心问题
|
||||
|
||||
| 类别 | 问题 | 严重度 |
|
||||
|---|---|---|
|
||||
| **接口不兼容** | Tool 注册使用自定义 interface,非 OpenClaw Plugin SDK | 🔴 致命 |
|
||||
| **Skill 格式错误** | 多行 YAML 嵌套结构(`arguments`、`allowed_tools`),OpenClaw 解析器只支持单行键值 | 🔴 致命 |
|
||||
| **无持久化** | in-memory Map,进程重启后记忆全部丢失 | 🔴 致命 |
|
||||
| **Schema 格式** | 手写 JSON Schema 对象,非 `@sinclair/typebox` | 🔴 致命 |
|
||||
| **返回值格式** | `JSON.stringify({success, data})` 非 `{ content: [{ type: "text", text }] }` | 🔴 致命 |
|
||||
| **无 Plugin 结构** | 缺少 `openclaw.plugin.json`、`package.json` 的 `openclaw` 字段 | 🔴 致命 |
|
||||
| **功能缺失** | depth 遍历、timeRange 过滤、supersede 模式、archive、cleanup、ToolLimiter | 🟡 中等 |
|
||||
| **无测试** | 全部删除,无新测试覆盖 | 🔴 严重 |
|
||||
| **命名不规范** | `graph_memory`(下划线)应为 kebab-case | 🟡 轻微 |
|
||||
|
||||
---
|
||||
|
||||
## OpenClaw 接口规范对照
|
||||
|
||||
### Tool 注册(官方 Plugin SDK)
|
||||
|
||||
```typescript
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "graph-memory",
|
||||
register(api) {
|
||||
api.registerTool({
|
||||
name: "graph_memory",
|
||||
description: "图记忆工具 - 让 AI 拥有真正的长期记忆能力",
|
||||
parameters: Type.Object({
|
||||
action: Type.String({ enum: ["recall", "commit", "purge", ...] }),
|
||||
params: Type.Object({ ... }),
|
||||
}),
|
||||
async execute(_id, params) {
|
||||
// 返回格式必须是:
|
||||
return { content: [{ type: "text", text: resultString }] };
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### SKILL.md 格式(官方要求)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: graph-memory
|
||||
description: "图记忆工具 - 检索、写入、删除记忆,管理人设和任务"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# 正文指令...
|
||||
```
|
||||
|
||||
**关键约束**:
|
||||
- `metadata` 必须是**单行 JSON 对象**
|
||||
- `description` 不能包含 `: `(冒号+空格),否则 YAML 解析**静默失败**
|
||||
- `name` 必须 kebab-case,与文件夹名匹配
|
||||
- **不支持** `arguments`、`allowed_tools`、`context: inline` 等多行 YAML 嵌套结构
|
||||
- 所有 frontmatter 键值必须是**单行**
|
||||
|
||||
### Plugin Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "graph-memory",
|
||||
"name": "Graph Memory",
|
||||
"description": "让 AI 拥有真正的长期记忆能力",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### package.json 扩展
|
||||
|
||||
```json
|
||||
{
|
||||
"openclaw": {
|
||||
"extensions": ["./dist/plugin-entry.js"],
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.3.24-beta.2",
|
||||
"minGatewayVersion": "2026.3.24-beta.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.3.24-beta.2",
|
||||
"pluginSdkVersion": "2026.3.24-beta.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### Phase 1: 项目结构改造为 OpenClaw Plugin(🔴 P0 - 阻塞)
|
||||
|
||||
#### 1.1 添加 OpenClaw Plugin SDK 依赖
|
||||
|
||||
- [ ] 1.1.1 `npm install @sinclair/typebox`
|
||||
- [ ] 1.1.2 更新 `ts/package.json` 添加 `openclaw` 字段(extensions、compat、build)
|
||||
- [ ] 1.1.3 创建 `ts/openclaw.plugin.json` manifest 文件
|
||||
|
||||
#### 1.2 创建 Plugin Entry Point
|
||||
|
||||
- [ ] 1.2.1 创建 `ts/src/plugin-entry.ts`
|
||||
- 使用 `definePluginEntry` 包裹
|
||||
- 通过 `api.registerTool()` 注册 GraphMemoryTool
|
||||
- 工具名称: `graph_memory`
|
||||
- 描述: "图记忆工具 - 让 AI 拥有真正的长期记忆能力"
|
||||
- [ ] 1.2.2 确保 entry point 导出为 ESM 格式
|
||||
- [ ] 1.2.3 更新 `ts/tsconfig.json` 确保编译输出路径正确
|
||||
|
||||
#### 1.3 迁移 Tool Schema 到 TypeBox
|
||||
|
||||
- [ ] 1.3.1 创建 `ts/src/runtime/core/tools/builtin/graph_memory_schema.ts`
|
||||
- 用 `Type.Object` 定义 action 参数
|
||||
- 用 `Type.Object` 定义 params 嵌套结构
|
||||
- 覆盖所有 10 个 action 的参数类型
|
||||
- [ ] 1.3.2 更新 `graph_memory_tool.ts` 的 `inputSchema` 字段为 TypeBox schema
|
||||
- [ ] 1.3.3 修改 `handler` 方法签名匹配 OpenClaw 的 `execute(_id, params)` 格式
|
||||
- [ ] 1.3.4 修改返回值格式为 `{ content: [{ type: "text", text: string }] }`
|
||||
|
||||
#### 1.4 更新 Tool Interface
|
||||
|
||||
- [ ] 1.4.1 更新 `ts/src/runtime/core/tools/tool_interface.ts`
|
||||
- 保持向后兼容(如其他模块引用)
|
||||
- 添加 OpenClaw 兼容的 `execute` 方法签名
|
||||
- 添加 `content` 返回类型定义
|
||||
|
||||
#### Phase 1 验收标准
|
||||
|
||||
- [ ] `npm run build` 编译通过
|
||||
- [ ] `openclaw.plugin.json` 格式正确
|
||||
- [ ] Plugin entry 使用 `definePluginEntry`
|
||||
- [ ] Tool 使用 `api.registerTool` 注册
|
||||
- [ ] Schema 使用 TypeBox
|
||||
- [ ] 返回值格式为 `{ content: [{ type: "text", text }] }`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: SKILL.md 格式修复(🔴 P0 - 阻塞)
|
||||
|
||||
#### 2.1 修复 `skills/graph_memory/SKILL.md`
|
||||
|
||||
- [ ] 2.1.1 移除 `when_to_use` 多行值(合并到 `description`)
|
||||
- [ ] 2.1.2 移除 `context: inline`(非 OpenClaw 标准字段)
|
||||
- [ ] 2.1.3 移除 `allowed_tools` 多行数组
|
||||
- [ ] 2.1.4 移除 `arguments` 多行嵌套结构
|
||||
- [ ] 2.1.5 `name` 改为 `graph-memory`(kebab-case)
|
||||
- [ ] 2.1.6 `description` 改为单行,不含 `: `
|
||||
- [ ] 2.1.7 添加 `metadata` 单行 JSON
|
||||
- [ ] 2.1.8 保留 `user_invocable: true`(改为 `user-invocable: true`,kebab-case)
|
||||
|
||||
**修复后格式**:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: graph-memory
|
||||
description: "图记忆工具 - 检索、写入、删除记忆,管理人设和任务。使用 recall 检索、commit 写入、purge 删除"
|
||||
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
|
||||
user-invocable: true
|
||||
---
|
||||
```
|
||||
|
||||
#### 2.2 修复 `skills/graph_memory/persona/SKILL.md`
|
||||
|
||||
- [ ] 2.2.1 移除 `when_to_use`、`context`、`allowed_tools`、`arguments`
|
||||
- [ ] 2.2.2 `name` 改为 `graph-memory-persona`
|
||||
- [ ] 2.2.3 `description` 改为单行
|
||||
- [ ] 2.2.4 添加 `metadata` 单行 JSON
|
||||
|
||||
#### 2.3 修复 `skills/graph_memory/task/SKILL.md`
|
||||
|
||||
- [ ] 2.3.1 移除 `when_to_use`、`context`、`allowed_tools`、`arguments`
|
||||
- [ ] 2.3.2 `name` 改为 `graph-memory-task`
|
||||
- [ ] 2.3.3 `description` 改为单行
|
||||
- [ ] 2.3.4 添加 `metadata` 单行 JSON
|
||||
|
||||
#### 2.4 同步修复 `ts/bundled-skills/` 下三个文件
|
||||
|
||||
- [ ] 2.4.1 `ts/bundled-skills/graph_memory/SKILL.md`
|
||||
- [ ] 2.4.2 `ts/bundled-skills/graph_memory/persona/SKILL.md`
|
||||
- [ ] 2.4.3 `ts/bundled-skills/graph_memory/task/SKILL.md`
|
||||
|
||||
#### 2.5 重命名目录(kebab-case)
|
||||
|
||||
- [ ] 2.5.1 `skills/graph_memory/` → `skills/graph-memory/`
|
||||
- [ ] 2.5.2 `skills/graph_memory/persona/` → `skills/graph-memory/persona/`
|
||||
- [ ] 2.5.3 `skills/graph_memory/task/` → `skills/graph-memory/task/`
|
||||
- [ ] 2.5.4 `ts/bundled-skills/graph_memory/` → `ts/bundled-skills/graph-memory/`
|
||||
- [ ] 2.5.5 同步更新 README.md 和 README_EN.md 中的路径引用
|
||||
|
||||
#### Phase 2 验收标准
|
||||
|
||||
- [ ] 所有 SKILL.md 的 frontmatter 仅含单行键值
|
||||
- [ ] `name` 全部 kebab-case
|
||||
- [ ] `description` 不含 `: `
|
||||
- [ ] `metadata` 为单行 JSON 对象
|
||||
- [ ] 无 `arguments`、`allowed_tools`、`context: inline` 等非标准字段
|
||||
- [ ] 目录名与 `name` 一致
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: GraphDatabase 持久化(🔴 P0 - 核心价值)
|
||||
|
||||
#### 3.1 添加 SQLite 依赖
|
||||
|
||||
- [ ] 3.1.1 `npm install better-sqlite3`
|
||||
- [ ] 3.1.2 `npm install @types/better-sqlite3 --save-dev`
|
||||
|
||||
#### 3.2 重写 GraphDatabase
|
||||
|
||||
- [ ] 3.2.1 修改构造函数接受 `dbPath` 参数
|
||||
- [ ] 3.2.2 使用 `better-sqlite3` 创建/连接数据库
|
||||
- [ ] 3.2.3 创建实体表(entities):
|
||||
- `id TEXT PRIMARY KEY`
|
||||
- `name TEXT UNIQUE NOT NULL`
|
||||
- `type TEXT`
|
||||
- `mention_count INTEGER DEFAULT 1`
|
||||
- `created_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- `updated_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- [ ] 3.2.4 创建关系表(relations):
|
||||
- `id TEXT PRIMARY KEY`
|
||||
- `source_id TEXT NOT NULL`(外键 → entities.id)
|
||||
- `target_id TEXT NOT NULL`(外键 → entities.id)
|
||||
- `relation_type TEXT NOT NULL`
|
||||
- `confidence REAL DEFAULT 1.0`
|
||||
- `status TEXT DEFAULT 'active'`
|
||||
- `session_id TEXT`
|
||||
- `turn_id INTEGER`
|
||||
- `created_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- `updated_at TEXT DEFAULT CURRENT_TIMESTAMP`
|
||||
- `date_bucket TEXT`
|
||||
- `superseded_by INTEGER`
|
||||
- [ ] 3.2.5 创建索引:
|
||||
- `idx_entity_name ON entities(name)`
|
||||
- `idx_entity_type ON entities(type)`
|
||||
- `idx_relation_source ON relations(source_id)`
|
||||
- `idx_relation_target ON relations(target_id)`
|
||||
- `idx_relation_type ON relations(relation_type)`
|
||||
- `idx_relation_status ON relations(status)`
|
||||
- `idx_relation_session ON relations(session_id)`
|
||||
- `idx_relation_date ON relations(date_bucket)`
|
||||
|
||||
#### 3.3 实现 recall 的 depth 多跳遍历
|
||||
|
||||
- [ ] 3.3.1 实现 BFS/DFS 图遍历算法
|
||||
- [ ] 3.3.2 depth=1: 直接匹配关键词的实体及其关系
|
||||
- [ ] 3.3.3 depth=2: 扩展到相邻实体的关系
|
||||
- [ ] 3.3.4 depth=N: 递归扩展到 N 层
|
||||
- [ ] 3.3.5 限制最大 depth 为 5(防止爆炸)
|
||||
- [ ] 3.3.6 去重已访问实体
|
||||
|
||||
#### 3.4 实现 recall 的 timeRange 过滤
|
||||
|
||||
- [ ] 3.4.1 解析 `timeRange.days` 参数
|
||||
- [ ] 3.4.2 在 SQL 查询中添加 `created_at >= datetime('now', '-N days')` 条件
|
||||
- [ ] 3.4.3 支持 `timeRange.from` 和 `timeRange.to` 范围查询
|
||||
|
||||
#### 3.5 实现 purge 的 supersede 模式
|
||||
|
||||
- [ ] 3.5.1 当 `mode === 'supersede'` 时:
|
||||
- 标记旧关系为 `superseded`
|
||||
- 设置 `superseded_by` 指向新关系 ID
|
||||
- 创建新关系(使用 `newRelation` 参数)
|
||||
- [ ] 3.5.2 更新 `PurgeParams` 类型支持 `newRelation` 字段
|
||||
|
||||
#### 3.6 实现 memory_archive 归档功能
|
||||
|
||||
- [ ] 3.6.1 添加 `archive(days: number)` 方法
|
||||
- [ ] 3.6.2 将 N 天前的非活跃关系标记为 `archived`
|
||||
- [ ] 3.6.3 在 recall 中排除 `archived` 状态的关系(除非显式查询)
|
||||
|
||||
#### 3.7 实现 memory_cleanup 清理功能
|
||||
|
||||
- [ ] 3.7.1 添加 `cleanup(dryRun: boolean)` 方法
|
||||
- [ ] 3.7.2 物理删除 `status = 'deleted'` 超过 90 天的关系
|
||||
- [ ] 3.7.3 删除孤立节点(无任何关系连接的实体)
|
||||
- [ ] 3.7.4 `dryRun=true` 时只返回将被删除的内容
|
||||
|
||||
#### 3.8 修复 isEntityDeleted 逻辑
|
||||
|
||||
- [ ] 3.8.1 当前逻辑有误:只要有一个关系被删就算实体被删
|
||||
- [ ] 3.8.2 修正为:实体本身无 deleted 状态,通过关系状态判断
|
||||
- [ ] 3.8.3 或者:在 entities 表中添加 `status` 字段
|
||||
|
||||
#### Phase 3 验收标准
|
||||
|
||||
- [ ] 数据持久化:写入后重启进程,数据仍然存在
|
||||
- [ ] recall depth 遍历正确返回 N 层关系
|
||||
- [ ] timeRange 过滤按时间正确筛选
|
||||
- [ ] supersede 模式正确标记替代关系
|
||||
- [ ] archive 正确归档旧数据
|
||||
- [ ] cleanup 正确清理无效数据
|
||||
- [ ] 编译通过,无类型错误
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: ToolLimiter 迁移(🟡 P2 - 优化)
|
||||
|
||||
#### 4.1 创建 ToolLimiter
|
||||
|
||||
- [ ] 4.1.1 创建 `ts/src/runtime/core/tools/tool_limiter.ts`
|
||||
- [ ] 4.1.2 移植 Python `ToolLimiter` 逻辑:
|
||||
- `ToolLimits` 配置类
|
||||
- `ToolCallCount` 计数类
|
||||
- `_classify_tool` 分类方法
|
||||
- `can_call` 检查方法
|
||||
- `record_call` 记录方法
|
||||
- `reset` 重置方法
|
||||
- [ ] 4.1.3 默认限制值:
|
||||
- persona_query_max: 1
|
||||
- persona_update_max: 1
|
||||
- task_query_max: 4
|
||||
- task_update_max: 5
|
||||
- memory_query_max: 20
|
||||
- memory_update_max: 10
|
||||
|
||||
#### 4.2 集成到 GraphMemoryTool
|
||||
|
||||
- [ ] 4.2.1 在 Tool 构造函数中初始化 ToolLimiter
|
||||
- [ ] 4.2.2 在 `execute` 方法中调用 `can_call` 检查
|
||||
- [ ] 4.2.3 调用成功后调用 `record_call` 记录
|
||||
- [ ] 4.2.4 每轮对话结束时调用 `reset` 重置计数
|
||||
|
||||
#### Phase 4 验收标准
|
||||
|
||||
- [ ] ToolLimiter 正确分类所有工具调用
|
||||
- [ ] 超过限制时返回明确的拒绝消息
|
||||
- [ ] 每轮对话计数正确重置
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 测试重建(🔴 P1 - 质量保障)
|
||||
|
||||
#### 5.1 GraphDatabase 测试
|
||||
|
||||
- [ ] 5.1.1 创建 `ts/tests/runtime/core/graph_memory/graph_database.test.ts`
|
||||
- [ ] 5.1.2 commit 测试:创建实体和关系
|
||||
- [ ] 5.1.3 recall 测试:按关键词检索、按 seedEntities 检索
|
||||
- [ ] 5.1.4 recall depth 测试:1层、2层、3层遍历
|
||||
- [ ] 5.1.5 recall timeRange 测试:按时间范围过滤
|
||||
- [ ] 5.1.6 purge soft 测试:软删除
|
||||
- [ ] 5.1.7 purge hard 测试:硬删除
|
||||
- [ ] 5.1.8 purge supersede 测试:纠错替代
|
||||
- [ ] 5.1.9 introspect 测试:返回统计
|
||||
- [ ] 5.1.10 持久化测试:重启后数据保留
|
||||
- [ ] 5.1.11 archive 测试:归档旧数据
|
||||
- [ ] 5.1.12 cleanup 测试:清理无效数据
|
||||
- [ ] 5.1.13 sessionFilter 测试:按会话过滤
|
||||
- [ ] 5.1.14 并发测试:多线程安全
|
||||
- [ ] 5.1.15 边界测试:空查询、超长字符串
|
||||
|
||||
#### 5.2 MemoryService 测试
|
||||
|
||||
- [ ] 5.2.1 创建 `ts/tests/runtime/core/graph_memory/memory_service.test.ts`
|
||||
- [ ] 5.2.2 updatePersona merge 测试
|
||||
- [ ] 5.2.3 updatePersona replace 测试
|
||||
- [ ] 5.2.4 clearPersona 测试
|
||||
- [ ] 5.2.5 createTask 测试
|
||||
- [ ] 5.2.6 setTaskState 测试
|
||||
- [ ] 5.2.7 deleteTask 测试
|
||||
- [ ] 5.2.8 linkInfoToTask 测试
|
||||
- [ ] 5.2.9 setSessionId/getSessionId 测试
|
||||
- [ ] 5.2.10 任务状态转换测试(进行中→已暂停→进行中→已完成)
|
||||
- [ ] 5.2.11 人设属性合并测试
|
||||
- [ ] 5.2.12 错误处理测试:无效参数
|
||||
|
||||
#### 5.3 GraphMemoryTool 测试
|
||||
|
||||
- [ ] 5.3.1 创建 `ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts`
|
||||
- [ ] 5.3.2 metadata 测试:id、name、category
|
||||
- [ ] 5.3.3 recall action 测试
|
||||
- [ ] 5.3.4 commit action 测试
|
||||
- [ ] 5.3.5 purge action 测试
|
||||
- [ ] 5.3.6 introspect action 测试
|
||||
- [ ] 5.3.7 persona_update action 测试
|
||||
- [ ] 5.3.8 persona_clear action 测试
|
||||
- [ ] 5.3.9 task_create action 测试
|
||||
- [ ] 5.3.10 task_set_state action 测试
|
||||
- [ ] 5.3.11 task_delete action 测试
|
||||
- [ ] 5.3.12 task_link_info action 测试
|
||||
- [ ] 5.3.13 未知 action 错误处理测试
|
||||
- [ ] 5.3.14 返回值格式测试:`{ content: [{ type: "text", text }] }`
|
||||
- [ ] 5.3.15 ToolLimiter 集成测试
|
||||
|
||||
#### 5.4 Plugin Entry 集成测试
|
||||
|
||||
- [ ] 5.4.1 创建 `ts/tests/plugin-entry.test.ts`
|
||||
- [ ] 5.4.2 Plugin 注册测试
|
||||
- [ ] 5.4.3 Tool 注册测试
|
||||
- [ ] 5.4.4 Schema 验证测试
|
||||
|
||||
#### Phase 5 验收标准
|
||||
|
||||
- [ ] `npm test` 全部通过(50+ 用例)
|
||||
- [ ] 无跳过(skip)的测试
|
||||
- [ ] 覆盖率 > 80%
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: 文档更新(🟢 P3 - 收尾)
|
||||
|
||||
#### 6.1 更新 README.md
|
||||
|
||||
- [ ] 6.1.1 更新标题:TrulyMEM → OpenClaw Graph Memory Plugin
|
||||
- [ ] 6.1.2 更新安装方式:`openclaw plugins install`
|
||||
- [ ] 6.1.3 更新使用示例
|
||||
- [ ] 6.1.4 更新目录结构说明
|
||||
- [ ] 6.1.5 更新 API 文档(反映 TypeBox schema)
|
||||
|
||||
#### 6.2 更新 README_EN.md
|
||||
|
||||
- [ ] 6.2.1 同步中文 README 的所有更新
|
||||
- [ ] 6.2.2 确保英文表达准确
|
||||
|
||||
#### 6.3 更新迁移设计文档
|
||||
|
||||
- [ ] 6.3.1 更新 `docs/integration/waterflow-design.md`
|
||||
- [ ] 6.3.2 添加 OpenClaw 接口适配说明
|
||||
- [ ] 6.3.3 更新架构图中 Plugin SDK 部分
|
||||
|
||||
#### Phase 6 验收标准
|
||||
|
||||
- [ ] README.md 和 README_EN.md 内容一致
|
||||
- [ ] 安装步骤可执行
|
||||
- [ ] API 文档与实际代码一致
|
||||
|
||||
---
|
||||
|
||||
## 目标目录结构(重构后)
|
||||
|
||||
```
|
||||
TrulyMEM-TrueHumanMEM/
|
||||
├── ts/
|
||||
│ ├── src/
|
||||
│ │ ├── plugin-entry.ts # [NEW] OpenClaw Plugin 入口
|
||||
│ │ └── runtime/core/
|
||||
│ │ ├── graph_memory/
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── types.ts
|
||||
│ │ │ ├── graph_database.ts # [REWRITE] SQLite 持久化
|
||||
│ │ │ └── memory_service.ts
|
||||
│ │ └── tools/
|
||||
│ │ ├── builtin/
|
||||
│ │ │ ├── graph_memory_tool.ts # [UPDATE] OpenClaw 兼容
|
||||
│ │ │ └── graph_memory_schema.ts # [NEW] TypeBox Schema
|
||||
│ │ ├── tool_interface.ts # [UPDATE] 添加 execute 签名
|
||||
│ │ └── tool_limiter.ts # [NEW] 调用限制器
|
||||
│ ├── bundled-skills/
|
||||
│ │ └── graph-memory/ # [RENAMED] kebab-case
|
||||
│ │ ├── SKILL.md # [FIXED] 单行 frontmatter
|
||||
│ │ ├── persona/
|
||||
│ │ │ └── SKILL.md # [FIXED]
|
||||
│ │ └── task/
|
||||
│ │ └── SKILL.md # [FIXED]
|
||||
│ ├── tests/
|
||||
│ │ └── runtime/core/
|
||||
│ │ ├── graph_memory/
|
||||
│ │ │ ├── graph_database.test.ts # [NEW]
|
||||
│ │ │ └── memory_service.test.ts # [NEW]
|
||||
│ │ └── tools/builtin/
|
||||
│ │ └── graph_memory_tool.test.ts # [NEW]
|
||||
│ ├── package.json # [UPDATE] 添加 openclaw 字段
|
||||
│ ├── tsconfig.json
|
||||
│ └── openclaw.plugin.json # [NEW] Plugin Manifest
|
||||
├── skills/
|
||||
│ └── graph-memory/ # [RENAMED] kebab-case
|
||||
│ ├── SKILL.md # [FIXED]
|
||||
│ ├── persona/
|
||||
│ │ └── SKILL.md # [FIXED]
|
||||
│ └── task/
|
||||
│ └── SKILL.md # [FIXED]
|
||||
├── docs/integration/waterflow-design.md # [UPDATE]
|
||||
├── README.md # [UPDATE]
|
||||
├── README_EN.md # [UPDATE]
|
||||
├── .gitignore
|
||||
└── LICENSE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖变更
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"yaml": "^2.8.3",
|
||||
"@sinclair/typebox": "^0.34.0",
|
||||
"better-sqlite3": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.2",
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 执行顺序与并行策略
|
||||
|
||||
```
|
||||
Phase 1 (P0) ──────────────────────────────────────┐
|
||||
1.1 依赖 ─→ 1.2 Entry ─→ 1.3 Schema ─→ 1.4 Interface │
|
||||
├── 必须最先完成
|
||||
Phase 2 (P0) ──────────────────────────────────────┤ 否则无法在 OpenClaw 中运行
|
||||
2.1-2.3 SKILL.md 修复(可并行) │
|
||||
2.4 bundled-skills 同步 │
|
||||
2.5 目录重命名 │
|
||||
│
|
||||
Phase 3 (P0) ──────────────────────────────────────┤
|
||||
3.1 SQLite 依赖 │
|
||||
3.2 GraphDatabase 重写 │
|
||||
3.3-3.7 功能补全(可部分并行) │
|
||||
3.8 逻辑修复 │
|
||||
│
|
||||
Phase 4 (P2) ──────────────────────────────────────┤ 优化项,可延后
|
||||
4.1 ToolLimiter 创建 │
|
||||
4.2 集成到 Tool │
|
||||
│
|
||||
Phase 5 (P1) ──────────────────────────────────────┘ 在 Phase 1-3 完成后执行
|
||||
5.1-5.4 测试重建(可并行编写)
|
||||
|
||||
Phase 6 (P3) ────────────────────────────────────────── 最后执行,文档收尾
|
||||
6.1-6.3 文档更新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|---|---|---|
|
||||
| better-sqlite3 原生模块编译失败 | 阻塞 Phase 3 | 使用预编译二进制或回退到 sql.js |
|
||||
| OpenClaw Plugin SDK 版本不兼容 | 阻塞 Phase 1 | 锁定 `compat.pluginApi` 版本 |
|
||||
| SKILL.md 描述中的中文冒号 | 静默加载失败 | 所有 description 用双引号包裹 |
|
||||
| SQLite 并发写入冲突 | 数据损坏 | 使用 WAL 模式 + 连接池 |
|
||||
| depth 遍历性能问题 | 响应缓慢 | 限制最大 depth=5,结果上限 100 |
|
||||
|
||||
---
|
||||
|
||||
## 验收总标准
|
||||
|
||||
- [ ] Phase 1-6 全部完成
|
||||
- [ ] `npm run build` 编译通过,无错误无警告
|
||||
- [ ] `npm test` 全部通过(50+ 用例)
|
||||
- [ ] 作为 OpenClaw Plugin 可安装、可加载、可调用
|
||||
- [ ] 数据持久化:写入后重启进程,数据仍然存在
|
||||
- [ ] 所有 main 分支的核心功能均已实现
|
||||
- [ ] SKILL.md 通过 OpenClaw 的 `openclaw skills check` 验证
|
||||
95
.sisyphus/plans/progress.md
Normal file
95
.sisyphus/plans/progress.md
Normal file
@ -0,0 +1,95 @@
|
||||
# 重构执行进度追踪
|
||||
|
||||
> 最后更新: 2026-04-16 15:00
|
||||
> 当前分支: openclaw
|
||||
|
||||
## Phase 1: 项目结构改造为 OpenClaw Plugin(🔴 P0)
|
||||
|
||||
### 1.1 添加 OpenClaw Plugin SDK 依赖
|
||||
- [x] 1.1.1 `npm install @sinclair/typebox better-sqlite3`
|
||||
- [x] 1.1.2 更新 `ts/package.json` 添加 `openclaw` 字段
|
||||
- [x] 1.1.3 创建 `ts/openclaw.plugin.json` manifest 文件
|
||||
|
||||
### 1.2 创建 Plugin Entry Point
|
||||
- [x] 1.2.1 创建 `ts/src/plugin-entry.ts`
|
||||
- [x] 1.2.2 确保 entry point 导出为 ESM 格式
|
||||
- [x] 1.2.3 更新 `ts/tsconfig.json` 确保编译输出路径正确
|
||||
|
||||
### 1.3 迁移 Tool Schema 到 TypeBox
|
||||
- [x] 1.3.1 添加 TypeBox schema 到 plugin-entry.ts
|
||||
- [x] 1.3.2 更新 `graph_memory_tool.ts` 的 `inputSchema` 字段(添加 newRelation, days, dry_run)
|
||||
- [x] 1.3.3 添加 `execute(_id, params)` 方法匹配 OpenClaw 签名
|
||||
- [x] 1.3.4 返回值格式为 `{ content: [{ type: "text", text }] }`
|
||||
|
||||
### 1.4 更新 Tool Interface
|
||||
- [x] 1.4.1 添加 `OpenClawToolResult` 类型到 graph_memory_tool.ts
|
||||
|
||||
## Phase 2: SKILL.md 格式修复(🔴 P0)
|
||||
|
||||
### 2.1 修复 skills/graph-memory/SKILL.md
|
||||
- [x] 2.1.1 移除多行嵌套结构
|
||||
- [x] 2.1.2 name 改为 kebab-case
|
||||
- [x] 2.1.3 description 改为单行
|
||||
- [x] 2.1.4 添加 metadata 单行 JSON
|
||||
|
||||
### 2.2 修复 skills/graph-memory/persona/SKILL.md
|
||||
- [x] 2.2.1 同上
|
||||
|
||||
### 2.3 修复 skills/graph-memory/task/SKILL.md
|
||||
- [x] 2.3.1 同上
|
||||
|
||||
### 2.4 同步修复 bundled-skills
|
||||
- [x] 2.4.1 ts/bundled-skills/graph-memory/SKILL.md
|
||||
- [x] 2.4.2 ts/bundled-skills/graph-memory/persona/SKILL.md
|
||||
- [x] 2.4.3 ts/bundled-skills/graph-memory/task/SKILL.md
|
||||
|
||||
### 2.5 重命名目录
|
||||
- [x] 2.5.1 skills/graph_memory/ → skills/graph-memory/
|
||||
- [x] 2.5.2 ts/bundled-skills/graph_memory/ → ts/bundled-skills/graph-memory/
|
||||
|
||||
## Phase 3: GraphDatabase 持久化(🔴 P0)
|
||||
|
||||
### 3.1 添加 SQLite 依赖
|
||||
- [ ] 3.1.1 npm install better-sqlite3
|
||||
- [ ] 3.1.2 npm install @types/better-sqlite3
|
||||
|
||||
### 3.2 重写 GraphDatabase
|
||||
- [x] 3.2.1 修改构造函数接受 dbPath
|
||||
- [x] 3.2.2 使用 better-sqlite3 创建/连接数据库
|
||||
- [x] 3.2.3 创建实体表
|
||||
- [x] 3.2.4 创建关系表
|
||||
- [x] 3.2.5 创建索引
|
||||
|
||||
### 3.3-3.7 功能补全
|
||||
- [x] 3.3 depth 多跳遍历 (BFS)
|
||||
- [x] 3.4 timeRange 过滤
|
||||
- [x] 3.5 supersede 模式
|
||||
- [x] 3.6 archive 归档
|
||||
- [x] 3.7 cleanup 清理
|
||||
|
||||
### 3.8 修复逻辑
|
||||
- [x] 3.8.1 修复 isEntityDeleted 逻辑 (已移除,用 SQL 替代)
|
||||
|
||||
## Phase 4: ToolLimiter(🟡 P2)
|
||||
|
||||
- [ ] 4.1 创建 ToolLimiter
|
||||
- [ ] 4.2 集成到 GraphMemoryTool
|
||||
|
||||
## Phase 5: 测试重建(🔴 P1)
|
||||
|
||||
- [ ] 5.1 GraphDatabase 测试 (15+)
|
||||
- [ ] 5.2 MemoryService 测试 (12+)
|
||||
- [ ] 5.3 GraphMemoryTool 测试 (15+)
|
||||
- [ ] 5.4 Plugin Entry 集成测试
|
||||
|
||||
## Phase 6: 文档更新(🟢 P3)
|
||||
|
||||
- [ ] 6.1 更新 README.md
|
||||
- [ ] 6.2 更新 README_EN.md
|
||||
- [ ] 6.3 更新迁移设计文档
|
||||
|
||||
## 最终验收
|
||||
|
||||
- [ ] npm run build 编译通过
|
||||
- [ ] npm test 全部通过
|
||||
- [ ] 推送到远程 openclaw 分支
|
||||
81
.sisyphus/plans/todo_progress.md
Normal file
81
.sisyphus/plans/todo_progress.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 迁移工作进度追踪
|
||||
|
||||
> ⚠️ **修改只在 TrulyMEM 的 waterflow 分支执行** ⚠️
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
- **开始时间**: 2026-04-15
|
||||
- **当前任务**: 迁移完成,等待测试
|
||||
- **最后更新**: 2026-04-15
|
||||
- **状态**: TypeScript 编译通过
|
||||
|
||||
---
|
||||
|
||||
## Phase 完成状态
|
||||
|
||||
### Phase 1: 项目结构
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 1.1 ts/ 目录 | ✅ done | |
|
||||
| 1.2 package.json | ✅ done | |
|
||||
| 1.3 tsconfig.json | ✅ done | |
|
||||
|
||||
### Phase 2: 核心库
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 2.1 types.ts | ✅ done | |
|
||||
| 2.2 graph_database.ts | ✅ done | |
|
||||
| 2.3 memory_service.ts | ✅ done | |
|
||||
| 2.4 index.ts | ✅ done | |
|
||||
|
||||
### Phase 3: Tool 接口
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 3.1 graph_memory_tool.ts | ✅ done | |
|
||||
| 3.2 tool_interface.ts | ✅ done | |
|
||||
|
||||
### Phase 4: Skill 定义
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 4.1 SKILL.md (主) | ✅ done | |
|
||||
| 4.2 persona/SKILL.md | ✅ done | |
|
||||
| 4.3 task/SKILL.md | ✅ done | |
|
||||
|
||||
### Phase 5: 验证
|
||||
|
||||
| 任务 | 状态 | 备注 |
|
||||
|------|------|------|
|
||||
| 5.1 编译 | ✅ done | TypeScript 编译通过 |
|
||||
| 5.2 测试 | ⏳ pending | |
|
||||
|
||||
---
|
||||
|
||||
## 创建的文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ts/package.json` | 项目配置 |
|
||||
| `ts/tsconfig.json` | TypeScript 配置 |
|
||||
| `ts/src/runtime/core/graph_memory/types.ts` | 类型定义 |
|
||||
| `ts/src/runtime/core/graph_memory/graph_database.ts` | 图数据库 |
|
||||
| `ts/src/runtime/core/graph_memory/memory_service.ts` | 记忆服务 |
|
||||
| `ts/src/runtime/core/graph_memory/index.ts` | 模块导出 |
|
||||
| `ts/src/runtime/core/tools/tool_interface.ts` | Tool 接口 |
|
||||
| `ts/src/runtime/core/tools/builtin/graph_memory_tool.ts` | GraphMemory Tool |
|
||||
| `ts/bundled-skills/graph_memory/SKILL.md` | 主 Skill |
|
||||
| `ts/bundled-skills/graph_memory/persona/SKILL.md` | Persona Skill |
|
||||
| `ts/bundled-skills/graph_memory/task/SKILL.md` | Task Skill |
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
|
||||
- 每次修改文件前后更新此文件
|
||||
- 记录每次修改的文件和操作
|
||||
- 方便意外终止后恢复任务
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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
196
LICENSE
Normal 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>.
|
||||
327
README.md
Normal file
327
README.md
Normal file
@ -0,0 +1,327 @@
|
||||
# TrulyMEM - AI 主要长期记忆系统
|
||||
|
||||
让 AI 拥有真正的长期记忆能力 - OpenClaw 框架插件版
|
||||
|
||||
[English Version](./README_EN.md)
|
||||
|
||||
---
|
||||
|
||||
## 定位声明
|
||||
|
||||
**TrulyMEM 是 AI 的主要长期记忆系统。**
|
||||
|
||||
与 OpenClaw 内置的 memory-core 并存运行:
|
||||
- **memory-core**:负责 session transcripts 和对话历史管理
|
||||
- **GraphMemory (TrulyMEM)**:负责**结构化长期记忆**——知识图谱、人设、任务、语义搜索
|
||||
|
||||
AI 在对话中应**优先**将重要信息(偏好、决策、任务、知识点)写入图数据库,而非依赖 message 历史。
|
||||
|
||||
---
|
||||
|
||||
## AI 自行安装指南
|
||||
|
||||
> 本节面向 AI Agent,按步骤指导你自主安装此插件到 OpenClaw 框架。
|
||||
|
||||
### 步骤 1:克隆仓库
|
||||
|
||||
```bash
|
||||
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git checkout openclaw
|
||||
```
|
||||
|
||||
### 步骤 2:编译 TypeScript 插件
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
编译成功后,`dist/` 目录会输出编译后的 JavaScript 文件。
|
||||
|
||||
### 步骤 3:安装 Skill(让 AI 能调用记忆工具)
|
||||
|
||||
将 `skills/` 目录下的 3 个 Skill 复制到 OpenClaw 的 Skill 目录。优先级:`<workspace>/skills/` > `~/.openclaw/skills/` > `~/.agents/skills/`
|
||||
|
||||
```bash
|
||||
# 方式 A:工作空间级(推荐,仅当前项目可用)
|
||||
cp -r skills/graph-memory <你的项目目录>/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona <你的项目目录>/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task <你的项目目录>/skills/graph-memory-task
|
||||
|
||||
# 方式 B:用户级(所有项目可用)
|
||||
cp -r skills/graph-memory ~/.openclaw/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.openclaw/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.openclaw/skills/graph-memory-task
|
||||
```
|
||||
|
||||
### 步骤 4:安装 Plugin(让 OpenClaw 运行时加载 Tool)
|
||||
|
||||
将编译后的 `ts/` 目录复制到 OpenClaw 的全局扩展目录:
|
||||
|
||||
```bash
|
||||
# 复制到 OpenClaw extensions 目录
|
||||
mkdir -p ~/.openclaw/extensions
|
||||
cp -r ts ~/.openclaw/extensions/graph-memory
|
||||
```
|
||||
|
||||
然后启用插件:
|
||||
|
||||
```bash
|
||||
# 启用插件
|
||||
openclaw plugins enable graph-memory
|
||||
|
||||
# 重启 Gateway(如果使用 systemd 托管)
|
||||
systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
验证插件已加载:
|
||||
|
||||
```bash
|
||||
openclaw plugins list
|
||||
# 应显示 graph-memory 为 loaded 状态
|
||||
```
|
||||
|
||||
**注意**:插件与 memory-core **并存运行**,不会禁用其他内存插件。memory-core 继续管理对话历史,GraphMemory 提供结构化长期记忆。
|
||||
|
||||
### 步骤 5:验证安装
|
||||
|
||||
启动 OpenClaw 后,发送以下消息测试:
|
||||
|
||||
```
|
||||
请记住:我喜欢编程,正在学习 TypeScript
|
||||
```
|
||||
|
||||
AI 应调用 `graph_memory` 工具的 `commit` action 并返回成功确认。
|
||||
|
||||
然后测试检索:
|
||||
|
||||
```
|
||||
我之前说过喜欢什么?
|
||||
```
|
||||
|
||||
AI 应调用 `recall` action 并返回之前写入的信息。
|
||||
|
||||
### 步骤 6:确认 Skill 已加载
|
||||
|
||||
在 OpenClaw 中执行:
|
||||
|
||||
```
|
||||
/skills
|
||||
```
|
||||
|
||||
你应该能看到以下 3 个 Skill:
|
||||
- `graph-memory` - 记忆 CRUD + 语义搜索 + 上下文压缩
|
||||
- `graph-memory-persona` - 人设管理
|
||||
- `graph-memory-task` - 任务管理
|
||||
|
||||
---
|
||||
|
||||
## 简介
|
||||
|
||||
本项目是 OpenClaw 的图记忆插件,基于 SQLite 实现持久化图数据库。
|
||||
|
||||
**设计理念:AI 的主要长期记忆系统**
|
||||
|
||||
本插件作为 AI 的**主要长期记忆存储**,与 memory-core 并存运行:
|
||||
- **memory-core 管理对话历史**:session transcripts 和历史消息由 memory-core 自动管理
|
||||
- **GraphMemory 管理结构化记忆**:重要事实、人设、任务、知识图谱由 AI 主动写入图数据库
|
||||
- **AI 优先使用图记忆**:对于持久信息,AI 应优先写入图数据库而非依赖 message 上下文
|
||||
|
||||
**核心功能:**
|
||||
|
||||
### 基础记忆操作
|
||||
- **recall**: 检索记忆(支持关键词、种子实体、多跳遍历、时间过滤)
|
||||
- **commit**: 写入记忆(三元组批量写入)
|
||||
- **purge**: 删除记忆(软删除/硬删除/纠错替代)
|
||||
- **introspect**: 查看记忆状态
|
||||
- **archive**: 归档旧记忆
|
||||
- **cleanup**: 清理无效数据
|
||||
|
||||
### 高级功能(P2)
|
||||
- **memory_search**: 语义搜索——基于本地 ONNX embedding 的向量相似度搜索
|
||||
- **memory_get**: 精确读取——按路径读取记忆文件内容片段
|
||||
- **context_rewrite**: 上下文压缩——将长对话历史压缩为关键记忆节点
|
||||
- **working_memory_chain**: 工作记忆链——获取当前会话的活跃关系链
|
||||
- **task_node_create/get_recent/get_chain**: 任务节点——创建和追踪连续性任务节点
|
||||
|
||||
### 人设与任务管理
|
||||
- **persona_update/clear**: 人设管理(AI 应主动查询人设指导行为)
|
||||
- **task_create/set_state/delete/link_info**: 任务管理(AI 应主动追踪任务状态)
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
### 方式一:作为 OpenClaw 插件安装
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
将插件目录添加到 OpenClaw 配置中,或使用 `openclaw plugins install` 安装。
|
||||
|
||||
### 方式二:作为 Skill 安装(推荐)
|
||||
|
||||
将 `skills/` 目录复制到 OpenClaw 的 Skill 目录:
|
||||
|
||||
```bash
|
||||
cp -r skills/graph-memory ~/.agents/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.agents/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ts/
|
||||
├── src/
|
||||
│ ├── plugin-entry.ts # OpenClaw Plugin 入口
|
||||
│ └── runtime/core/
|
||||
│ ├── graph_memory/
|
||||
│ │ ├── types.ts # 类型定义
|
||||
│ │ ├── graph_database.ts # SQLite 图数据库
|
||||
│ │ ├── memory_service.ts # 记忆服务
|
||||
│ │ ├── semantic_search.ts # 语义搜索引擎(P2)
|
||||
│ │ └── index.ts # 模块导出
|
||||
│ └── tools/
|
||||
│ ├── builtin/
|
||||
│ │ └── graph_memory_tool.ts # Tool 实现
|
||||
│ └── tool_limiter.ts # 调用限制器
|
||||
├── bundled-skills/
|
||||
│ ├── graph-memory/ # 内置 Skill 定义
|
||||
│ │ └── SKILL.md
|
||||
│ ├── graph-memory-persona/
|
||||
│ │ └── SKILL.md
|
||||
│ └── graph-memory-task/
|
||||
│ └── SKILL.md
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── openclaw.plugin.json # Plugin Manifest
|
||||
|
||||
skills/ # 独立 Skill 定义
|
||||
├── graph-memory/
|
||||
│ └── SKILL.md
|
||||
├── graph-memory-persona/
|
||||
│ └── SKILL.md
|
||||
└── graph-memory-task/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在 OpenClaw 中使用
|
||||
|
||||
### 作为 Plugin
|
||||
|
||||
插件入口导出符合 OpenClaw SDK 规范的对象:
|
||||
|
||||
```typescript
|
||||
// plugin-entry.ts 导出格式
|
||||
export default {
|
||||
id: 'graph-memory',
|
||||
name: 'Graph Memory',
|
||||
description: '让 AI 拥有真正的长期记忆能力',
|
||||
register(api) {
|
||||
api.registerTool(tool);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
OpenClaw 加载后会自动调用 `register(api)` 注册工具。
|
||||
|
||||
### 作为独立模块
|
||||
|
||||
```typescript
|
||||
import { createGraphMemoryTool } from './dist/runtime/core/tools/builtin/graph_memory_tool.js';
|
||||
|
||||
const tool = createGraphMemoryTool('graph_memory.db', 'my-session-id');
|
||||
|
||||
// 写入记忆
|
||||
const result = await tool.execute('call-1', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: '编程' },
|
||||
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// 语义搜索
|
||||
const searchResult = await tool.execute('call-2', {
|
||||
action: 'memory_search',
|
||||
params: { query: '编程相关', limit: 5 }
|
||||
});
|
||||
|
||||
// 检索记忆
|
||||
const recallResult = await tool.execute('call-3', {
|
||||
action: 'recall',
|
||||
params: { queryIntent: '用户 编程' }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Actions
|
||||
|
||||
| Action | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| `recall` | 检索记忆 | `queryIntent`, `seedEntities`, `depth`, `sessionFilter`, `timeRange` |
|
||||
| `commit` | 写入记忆 | `triplets`, `sessionId`, `turnId` |
|
||||
| `purge` | 删除记忆 | `criteria`, `mode` (soft/hard/supersede), `newRelation` |
|
||||
| `introspect` | 查看状态 | - |
|
||||
| `archive` | 归档旧记忆 | `days` (默认 30) |
|
||||
| `cleanup` | 清理无效数据 | `dry_run` (默认 true) |
|
||||
| **语义搜索** | | |
|
||||
| `memory_search` | 语义向量搜索 | `query`, `limit`, `corpus` |
|
||||
| `memory_get` | 精确读取记忆文件 | `path`, `fromLine`, `lines` |
|
||||
| **上下文压缩** | | |
|
||||
| `context_rewrite` | 压缩长对话为记忆节点 | `context`, `maxEntities`, `summary` |
|
||||
| `working_memory_chain` | 获取当前会话活跃关系链 | `maxDepth`, `recentOnly` |
|
||||
| **任务节点** | | |
|
||||
| `task_node_create` | 创建任务节点 | `session_id`, `turn_id`, `summary`, `key_facts` |
|
||||
| `task_node_get_recent` | 获取最近任务节点 | `session_id`, `limit` |
|
||||
| `task_node_get_chain` | 获取任务链 | `session_id`, `from_node_id` |
|
||||
| **人设管理** | | |
|
||||
| `persona_update` | 更新人设 | `attributes`, `mode` (merge/replace) |
|
||||
| `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` |
|
||||
|
||||
---
|
||||
|
||||
## Skill 列表
|
||||
|
||||
| Skill 名称 | 功能 | 使用场景 |
|
||||
|------------|------|----------|
|
||||
| `graph-memory` | 记忆 CRUD + 语义搜索 + 上下文压缩 | 读取/写入/删除记忆,语义搜索,压缩历史 |
|
||||
| `graph-memory-persona` | 人设管理 | 设置 AI 角色性格,AI 主动查询人设 |
|
||||
| `graph-memory-task` | 任务管理 | 创建/更新长期任务,AI 主动追踪任务 |
|
||||
|
||||
---
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build # 编译
|
||||
npm test # 运行测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
[GNU General Public License v3.0 (GPLv3)](LICENSE)
|
||||
261
README_EN.md
Normal file
261
README_EN.md
Normal file
@ -0,0 +1,261 @@
|
||||
# TrulyMEM - OpenClaw Graph Memory Plugin
|
||||
|
||||
Give AI true long-term memory capability - OpenClaw framework plugin version
|
||||
|
||||
[中文版本](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## AI Self-Installation Guide
|
||||
|
||||
> This section is for AI Agents. Follow these steps to install this plugin into your OpenClaw framework.
|
||||
|
||||
### Step 1: Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git checkout openclaw
|
||||
```
|
||||
|
||||
### Step 2: Build the TypeScript plugin
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
After successful build, the `dist/` directory contains compiled JavaScript files.
|
||||
|
||||
### Step 3: Install Skills (enables AI to call memory tools)
|
||||
|
||||
Copy the 3 Skills from `skills/` to OpenClaw's skill directory. Priority: `<workspace>/skills/` > `~/.openclaw/skills/` > `~/.agents/skills/`
|
||||
|
||||
```bash
|
||||
# Option A: Workspace-level (recommended, current project only)
|
||||
cp -r skills/graph-memory <your-project-dir>/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona <your-project-dir>/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task <your-project-dir>/skills/graph-memory-task
|
||||
|
||||
# Option B: User-level (available for all projects)
|
||||
cp -r skills/graph-memory ~/.openclaw/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.openclaw/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.openclaw/skills/graph-memory-task
|
||||
```
|
||||
|
||||
### Step 4: Install Plugin (enables OpenClaw runtime to load the Tool)
|
||||
|
||||
Load the `ts/` directory as an OpenClaw plugin. Add to `openclaw.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
"plugins": {
|
||||
"allow": ["@trulymem/openclaw-graph-memory"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or symlink the compiled `ts/dist/` directory to OpenClaw's plugin directory:
|
||||
|
||||
```bash
|
||||
ln -s <path-to-ts/dist> ~/.openclaw/plugins/graph-memory
|
||||
```
|
||||
|
||||
### Step 5: Verify installation
|
||||
|
||||
After starting OpenClaw, send this message to test:
|
||||
|
||||
```
|
||||
Please remember: I like programming and I'm learning TypeScript
|
||||
```
|
||||
|
||||
The AI should call the `graph_memory` tool's `commit` action and return a success confirmation.
|
||||
|
||||
Then test recall:
|
||||
|
||||
```
|
||||
What did I say I like?
|
||||
```
|
||||
|
||||
The AI should call the `recall` action and return the previously stored information.
|
||||
|
||||
### Step 6: Confirm Skills are loaded
|
||||
|
||||
In OpenClaw, run:
|
||||
|
||||
```
|
||||
/skills
|
||||
```
|
||||
|
||||
You should see these 3 Skills:
|
||||
- `graph-memory` - Memory CRUD
|
||||
- `graph-memory-persona` - Persona management
|
||||
- `graph-memory-task` - Task management
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
This project is an OpenClaw plugin for graph-based memory with SQLite persistence.
|
||||
|
||||
**Core Features:**
|
||||
- **recall**: Retrieve memories (keyword, seed entities, multi-hop traversal, time filtering)
|
||||
- **commit**: Write memories (batch triplet writes)
|
||||
- **purge**: Delete memories (soft/hard/supersede modes)
|
||||
- **introspect**: View memory statistics
|
||||
- **archive**: Archive old memories
|
||||
- **cleanup**: Clean up invalid data
|
||||
- **persona_update/clear**: Persona management
|
||||
- **task_create/set_state/delete/link_info**: Task management
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Method 1: As OpenClaw Plugin
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Add the plugin directory to your OpenClaw config, or use `openclaw plugins install`.
|
||||
|
||||
### Method 2: As Skill (Recommended)
|
||||
|
||||
Copy the `skills/` directory to OpenClaw's skill directory:
|
||||
|
||||
```bash
|
||||
cp -r skills/graph-memory ~/.agents/skills/graph-memory
|
||||
cp -r skills/graph-memory-persona ~/.agents/skills/graph-memory-persona
|
||||
cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
ts/
|
||||
├── src/
|
||||
│ ├── plugin-entry.ts # OpenClaw Plugin entry point
|
||||
│ └── runtime/core/
|
||||
│ ├── graph_memory/
|
||||
│ │ ├── types.ts # Type definitions
|
||||
│ │ ├── graph_database.ts # SQLite graph database
|
||||
│ │ ├── memory_service.ts # Memory service
|
||||
│ │ └── index.ts # Module exports
|
||||
│ └── tools/
|
||||
│ ├── builtin/
|
||||
│ │ └── graph_memory_tool.ts # Tool implementation
|
||||
│ └── tool_limiter.ts # Call rate limiter
|
||||
├── bundled-skills/
|
||||
│ ├── graph-memory/ # Bundled Skill definitions
|
||||
│ │ └── SKILL.md
|
||||
│ ├── graph-memory-persona/
|
||||
│ │ └── SKILL.md
|
||||
│ └── graph-memory-task/
|
||||
│ └── SKILL.md
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── openclaw.plugin.json # Plugin Manifest
|
||||
|
||||
skills/ # Standalone Skill definitions
|
||||
├── graph-memory/
|
||||
│ └── SKILL.md
|
||||
├── graph-memory-persona/
|
||||
│ └── SKILL.md
|
||||
└── graph-memory-task/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage in OpenClaw
|
||||
|
||||
### As Plugin
|
||||
|
||||
```typescript
|
||||
import registerGraphMemoryPlugin from './dist/plugin-entry.js';
|
||||
|
||||
registerGraphMemoryPlugin({
|
||||
registerTool(tool) {
|
||||
// OpenClaw will auto-register the tool
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### As Standalone Module
|
||||
|
||||
```typescript
|
||||
import { createGraphMemoryTool } from './dist/runtime/core/tools/builtin/graph_memory_tool.js';
|
||||
|
||||
const tool = createGraphMemoryTool('graph_memory.db', 'my-session-id');
|
||||
|
||||
// Write memory
|
||||
const result = await tool.execute('call-1', {
|
||||
action: 'commit',
|
||||
params: {
|
||||
triplets: [
|
||||
{ subject: 'User', relation: 'likes', object: 'programming' },
|
||||
{ subject: 'User', relation: 'learning', object: 'TypeScript' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Recall memory
|
||||
const recallResult = await tool.execute('call-2', {
|
||||
action: 'recall',
|
||||
params: { queryIntent: 'User programming' }
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Actions
|
||||
|
||||
| Action | Description | Parameters |
|
||||
|--------|-------------|------------|
|
||||
| `recall` | Retrieve memories | `queryIntent`, `seedEntities`, `depth`, `sessionFilter`, `timeRange` |
|
||||
| `commit` | Write memories | `triplets`, `sessionId`, `turnId` |
|
||||
| `purge` | Delete memories | `criteria`, `mode` (soft/hard/supersede), `newRelation` |
|
||||
| `introspect` | View status | - |
|
||||
| `archive` | Archive old memories | `days` (default 30) |
|
||||
| `cleanup` | Clean invalid data | `dry_run` (default true) |
|
||||
| `persona_update` | Update persona | `attributes`, `mode` (merge/replace) |
|
||||
| `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 | `task_id`, `info_node` |
|
||||
|
||||
---
|
||||
|
||||
## Skills
|
||||
|
||||
| Skill Name | Function | Use Case |
|
||||
|------------|----------|----------|
|
||||
| `graph-memory` | Memory CRUD | Read/write/delete memories |
|
||||
| `graph-memory-persona` | Persona management | Set AI role/personality |
|
||||
| `graph-memory-task` | Task management | Create/update long-term tasks |
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd ts/
|
||||
npm install
|
||||
npm run build # Compile
|
||||
npm test # Run tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[GNU General Public License v3.0 (GPLv3)](LICENSE)
|
||||
48
TODO.md
Normal file
48
TODO.md
Normal file
@ -0,0 +1,48 @@
|
||||
# TrulyMEM 待完成事项
|
||||
|
||||
## 项目概述
|
||||
TrulyMEM - 真正的长期记忆系统 (True Human MEMory)
|
||||
为 OpenClaw 提供图数据库形式的结构化长期记忆能力。
|
||||
|
||||
## 已完成功能
|
||||
|
||||
### P0 - 核心功能修复 ✅
|
||||
- [x] 确认移除 memory 插槽后的插件加载状态
|
||||
- [x] 测试与 memory-core 并存运行
|
||||
- [x] 验证工具 schema 正确传递给 Kimi
|
||||
|
||||
### P1 - 功能完善 ✅
|
||||
- [x] 4. 实现完整的工具参数验证
|
||||
- 为所有 action(recall/commit/purge/persona_update/persona_clear/task_create/task_set_state/task_delete/task_link_info)实现独立验证函数
|
||||
- 验证规则:recall 必需 queryIntent 或 seedEntities;depth 1-5;commit triplets 非空且字段有效;persona_clear 需 confirm;task 需 task_id 和 description 等
|
||||
- [x] 5. 添加错误处理和日志
|
||||
- 新增 GraphMemoryLogger 日志系统(info/warn/error/action 级别)
|
||||
- 敏感数据脱敏:attributes 只记录属性名,triplets 只记录数量
|
||||
- 参数验证错误返回 validation_error 类型
|
||||
- 执行错误返回 execution_error 类型
|
||||
- [x] 6. 完善 skill 文档(说明增强而非替换)
|
||||
- 更新 3 个 skill 文档(graph-memory、graph-memory-persona、graph-memory-task)
|
||||
- 明确说明是 OpenClaw memory-core 的增强补充,不替代核心功能
|
||||
- 添加与 memory-core 的关系对比表
|
||||
|
||||
## 进行中 / 待完成
|
||||
|
||||
### P2 - 可选高级功能
|
||||
- [ ] 7. 实现 context_rewrite 工具(压缩上下文)
|
||||
- [ ] 8. 实现工作记忆链机制
|
||||
- [ ] 9. 实现人设强制查询(作为 skill 而非核心)
|
||||
|
||||
## 技术规格
|
||||
|
||||
### 测试覆盖
|
||||
- 测试文件:`ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts`
|
||||
- 当前测试数:**118 个全部通过**
|
||||
- 参数验证测试:11 个(覆盖所有 action 的必填参数、范围校验等)
|
||||
|
||||
### 提交记录
|
||||
- 最新提交:`P1: 完整参数验证 + 错误处理/日志 + 测试覆盖`
|
||||
|
||||
## 注意事项
|
||||
- 所有功能均作为 OpenClaw 插件实现,不修改 OpenClaw 核心代码
|
||||
- 插件入口:`ts/src/plugin-entry.ts`
|
||||
- 技能目录:`skills/`(源文件)和 `ts/bundled-skills/`(编译后)
|
||||
@ -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,
|
||||
)
|
||||
@ -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
135
build.log
@ -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!
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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";
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/common
|
||||
@ -1,6 +0,0 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "@ohos/common",
|
||||
"version": "1.0.0",
|
||||
"description": "TrulyMEM common module",
|
||||
"main": "Index.ets",
|
||||
"author": "",
|
||||
"license": "",
|
||||
"dependencies": {}
|
||||
}
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
@ -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
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 `你是 TrulyMEM(True 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
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "common",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM common module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
{"SECRET_KEY": "3a38a0f46a76673154b491a7b061c38f2f6a55489078ae7b5d34e94e75fc0534"}
|
||||
811
docs/integration/waterflow-design.md
Normal file
811
docs/integration/waterflow-design.md
Normal file
@ -0,0 +1,811 @@
|
||||
# TrulyMEM → WaterFlow 迁移设计文档
|
||||
|
||||
**版本**: 1.0
|
||||
**日期**: 2026-04-15
|
||||
**目标**: 用 TypeScript 完全重写 TrulyMEM 的图记忆能力,集成到 WaterFlow
|
||||
|
||||
---
|
||||
|
||||
## 一、迁移策略
|
||||
|
||||
### 1.1 核心原则
|
||||
|
||||
- **完全重写**: 不保留 Python 代码,用 TypeScript 实现
|
||||
- **架构一致**: 遵循 WaterFlow 的架构风格和设计模式
|
||||
- **原生集成**: 作为 WaterFlow 的内置模块,而非外部依赖
|
||||
|
||||
### 1.2 迁移范围
|
||||
|
||||
| TrulyMEM (Python) | WaterFlow (TypeScript) | 说明 |
|
||||
|-------------------|------------------------|------|
|
||||
| `EmbeddedGraphDB` | `GraphDatabase` | SQLite 图数据库重写 |
|
||||
| `GraphMemoryClient` | `MemoryService` | 记忆服务 |
|
||||
| 12 个记忆工具 | `GraphMemoryTool` | WaterFlow Tool 接口 |
|
||||
| System Prompt | 提示词模板 | 提示词管理 |
|
||||
| TUI | ❌ 不迁移 | WaterFlow 无 TUI |
|
||||
|
||||
---
|
||||
|
||||
## 二、架构设计
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ WaterFlow Core │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||
│ │ Agent / │───>│ Query │───>│ ToolExecutor │ │
|
||||
│ │ Workflow │ │ Engine │ │ │ │
|
||||
│ └─────────────┘ └──────────────┘ └───────────┬────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ GraphMemory Module (NEW) │ │
|
||||
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
|
||||
│ │ │ GraphMemoryTool │ │ GraphDatabase │ │ MemoryService │ │ │
|
||||
│ │ │ (Tool Interface)│ │ (SQLite Graph) │ │ (LLM Integration)│ │ │
|
||||
│ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ └────────────────────┼────────────────────┘ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌─────────────────────┐ │ │
|
||||
│ │ │ GraphMemoryStore │ │ │
|
||||
│ │ │ (In-Memory Cache) │ │ │
|
||||
│ │ └─────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 模块职责
|
||||
|
||||
| 模块 | 职责 | 位置 |
|
||||
|------|------|------|
|
||||
| `GraphMemoryTool` | WaterFlow Tool 接口,暴露记忆能力 | `runtime/core/tools/builtin/graph_memory/` |
|
||||
| `GraphDatabase` | SQLite 图数据库实现 | `runtime/core/graph_memory/database/` |
|
||||
| `MemoryService` | 封装业务逻辑 | `runtime/core/graph_memory/service/` |
|
||||
| `GraphMemoryStore` | 内存缓存,加速查询 | `runtime/core/graph_memory/store/` |
|
||||
| `SystemPrompt` | 提示词模板管理 | `runtime/core/graph_memory/prompts/` |
|
||||
|
||||
---
|
||||
|
||||
## 三、目录结构
|
||||
|
||||
### 3.1 新增目录
|
||||
|
||||
```
|
||||
WaterFlow/ts/src/
|
||||
├── runtime/core/
|
||||
│ ├── graph_memory/ # 新增: 图记忆模块
|
||||
│ │ ├── index.ts # 模块导出
|
||||
│ │ ├── types.ts # 类型定义
|
||||
│ │ ├── database/ # 图数据库实现
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── graph_database.ts # 主类
|
||||
│ │ │ ├── entity_store.ts # 实体存储
|
||||
│ │ │ └── relation_store.ts # 关系存储
|
||||
│ │ ├── service/ # 服务层
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── memory_service.ts # 记忆服务
|
||||
│ │ │ ├── recall_service.ts # 检索服务
|
||||
│ │ │ └── task_service.ts # 任务服务
|
||||
│ │ ├── store/ # 缓存层
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ └── memory_cache.ts
|
||||
│ │ └── prompts/ # 提示词
|
||||
│ │ └── system_prompt.ts
|
||||
│ │
|
||||
│ └── tools/builtin/
|
||||
│ └── graph_memory/ # GraphMemory Tool
|
||||
│ ├── index.ts
|
||||
│ ├── graph_memory_tool.ts # Tool 实现
|
||||
│ ├── types.ts # Tool 参数类型
|
||||
│ └── tool_registry.ts # 自动注册
|
||||
```
|
||||
|
||||
### 3.2 修改文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `runtime/core/tools/builtin/index.ts` | 注册 GraphMemoryTool |
|
||||
| `shared/types/index.ts` | 导出图记忆类型 |
|
||||
|
||||
---
|
||||
|
||||
## 四、核心类型定义
|
||||
|
||||
### 4.1 图数据库类型
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/graph_memory/types.ts
|
||||
|
||||
export interface Entity {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
mentionCount: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Relation {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
relationType: string;
|
||||
confidence: number;
|
||||
status: RelationStatus;
|
||||
sessionId: string;
|
||||
turnId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dateBucket: string;
|
||||
}
|
||||
|
||||
export type RelationStatus = 'active' | 'deleted' | 'archived' | 'superseded';
|
||||
|
||||
export interface Triplet {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export interface RecallParams {
|
||||
queryIntent: string;
|
||||
seedEntities?: string[];
|
||||
depth?: number;
|
||||
timeRange?: { days: number };
|
||||
sessionFilter?: string;
|
||||
}
|
||||
|
||||
export interface CommitParams {
|
||||
triplets: Triplet[];
|
||||
entityTypes?: Record<string, string>;
|
||||
temporalTag?: string;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
}
|
||||
|
||||
export interface PurgeParams {
|
||||
criteria: {
|
||||
subject?: string;
|
||||
target?: string;
|
||||
relation?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
mode?: 'soft' | 'hard' | 'supersede';
|
||||
newRelation?: { relation: string; target: string };
|
||||
}
|
||||
|
||||
export type TaskState = '进行中' | '已完成' | '已暂停' | '已取消';
|
||||
|
||||
export interface MemoryStats {
|
||||
entityCount: number;
|
||||
relationCount: number;
|
||||
sessionId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Tool 参数类型
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/tools/builtin/graph_memory/types.ts
|
||||
|
||||
export type GraphMemoryAction =
|
||||
| 'recall' | 'commit' | 'purge' | 'introspect' | 'archive' | 'cleanup'
|
||||
| 'persona_update' | 'persona_clear'
|
||||
| 'task_create' | 'task_set_state' | 'task_delete' | 'task_link_info';
|
||||
|
||||
export interface GraphMemoryToolInput {
|
||||
action: GraphMemoryAction;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、核心实现
|
||||
|
||||
### 5.1 GraphDatabase 实现
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/graph_memory/database/graph_database.ts
|
||||
|
||||
export class GraphDatabase {
|
||||
private db: Database;
|
||||
|
||||
constructor(dbPath: string) {
|
||||
this.db = new Database(dbPath);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT,
|
||||
mention_count INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
confidence REAL DEFAULT 1.0,
|
||||
status TEXT DEFAULT 'active',
|
||||
session_id TEXT,
|
||||
turn_id INTEGER,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
date_bucket TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
// 索引
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status);
|
||||
`);
|
||||
}
|
||||
|
||||
async recall(params: RecallParams): Promise<RecallResult> {
|
||||
const { queryIntent, seedEntities, sessionFilter } = params;
|
||||
const keywords = queryIntent.split(/[,\s]+/).filter(k => k.length > 0);
|
||||
const entities: Entity[] = [];
|
||||
const relations: Relation[] = [];
|
||||
const entityIds = new Set<string>();
|
||||
|
||||
// 搜索实体
|
||||
for (const keyword of keywords) {
|
||||
const rows = this.db.exec(
|
||||
`SELECT * FROM entities WHERE LOWER(name) LIKE ? LIMIT 50`,
|
||||
[`%${keyword.toLowerCase()}%`]
|
||||
);
|
||||
for (const row of rows) {
|
||||
if (!entityIds.has(row.id)) {
|
||||
entityIds.add(row.id);
|
||||
entities.push(this.rowToEntity(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索关系
|
||||
if (entityIds.size > 0) {
|
||||
const placeholders = Array.from(entityIds).map(() => '?').join(',');
|
||||
let query = `
|
||||
SELECT r.*, e1.name as source_name, e2.name as target_name
|
||||
FROM relations r
|
||||
JOIN entities e1 ON r.source_id = e1.id
|
||||
JOIN entities e2 ON r.target_id = e2.id
|
||||
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
|
||||
AND r.status = 'active'
|
||||
`;
|
||||
const queryParams = [...entityIds, ...entityIds];
|
||||
|
||||
if (sessionFilter) {
|
||||
query += ` AND r.session_id = ?`;
|
||||
queryParams.push(sessionFilter);
|
||||
}
|
||||
|
||||
const rows = this.db.exec(query, queryParams);
|
||||
for (const row of rows) {
|
||||
relations.push(this.rowToRelation(row));
|
||||
}
|
||||
}
|
||||
|
||||
return { entities, relations, message: `找到 ${entities.length} 个实体, ${relations.length} 条关系` };
|
||||
}
|
||||
|
||||
async commit(params: CommitParams): Promise<{ createdEntities: number; createdRelations: number }> {
|
||||
const { triplets, sessionId, turnId } = params;
|
||||
let createdEntities = 0;
|
||||
let createdRelations = 0;
|
||||
|
||||
for (const triplet of triplets) {
|
||||
const sourceId = this.upsertEntity(triplet.subject);
|
||||
const targetId = this.upsertEntity(triplet.object);
|
||||
|
||||
this.db.exec(`
|
||||
INSERT INTO relations (id, source_id, target_id, relation_type, confidence, session_id, turn_id, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
|
||||
`, [this.generateId(), sourceId, targetId, triplet.relation, triplet.confidence || 1.0, sessionId, turnId || 0]);
|
||||
|
||||
createdEntities += 2;
|
||||
createdRelations++;
|
||||
}
|
||||
|
||||
return { createdEntities, createdRelations };
|
||||
}
|
||||
|
||||
async purge(params: PurgeParams): Promise<{ deleted: number; mode: string }> {
|
||||
const { criteria, mode = 'soft' } = params;
|
||||
const conditions: string[] = ['status = ?'];
|
||||
const values: unknown[] = ['active'];
|
||||
|
||||
if (criteria.subject) {
|
||||
conditions.push(`source_id IN (SELECT id FROM entities WHERE name = ?)`);
|
||||
values.push(criteria.subject);
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
const result = this.db.exec(`UPDATE relations SET status = 'deleted' WHERE ${whereClause}`, values);
|
||||
|
||||
return { deleted: result.length, mode };
|
||||
}
|
||||
|
||||
async introspect(): Promise<MemoryStats> {
|
||||
const entityCount = this.db.exec(`SELECT COUNT(*) as c FROM entities`)[0]?.c || 0;
|
||||
const relationCount = this.db.exec(`SELECT COUNT(*) as c FROM relations WHERE status = 'active'`)[0]?.c || 0;
|
||||
return { entityCount, relationCount };
|
||||
}
|
||||
|
||||
private upsertEntity(name: string): string {
|
||||
const existing = this.db.exec(`SELECT id FROM entities WHERE name = ?`, [name]);
|
||||
if (existing.length > 0) {
|
||||
this.db.exec(`UPDATE entities SET mention_count = mention_count + 1 WHERE name = ?`, [name]);
|
||||
return existing[0].id;
|
||||
}
|
||||
const id = this.generateId();
|
||||
this.db.exec(`INSERT INTO entities (id, name, type) VALUES (?, ?, ?)`, [id, name, 'unknown']);
|
||||
return id;
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
private rowToEntity(row: any): Entity {
|
||||
return {
|
||||
id: row.id, name: row.name, type: row.type || 'unknown',
|
||||
mentionCount: row.mention_count || 1,
|
||||
createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at)
|
||||
};
|
||||
}
|
||||
|
||||
private rowToRelation(row: any): Relation {
|
||||
return {
|
||||
id: row.id, sourceId: row.source_id, targetId: row.target_id,
|
||||
relationType: row.relation_type, confidence: row.confidence || 1.0,
|
||||
status: row.status || 'active', sessionId: row.session_id || '',
|
||||
turnId: row.turn_id || 0,
|
||||
createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at),
|
||||
dateBucket: row.date_bucket || ''
|
||||
};
|
||||
}
|
||||
|
||||
close(): void { this.db.close(); }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 GraphMemoryTool 实现
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/tools/builtin/graph_memory/graph_memory_tool.ts
|
||||
|
||||
import type { Tool, ToolExecutionContext, ToolInputSchema } from '../tool_interface';
|
||||
import { GraphDatabase } from '../../../graph_memory/database/graph_database';
|
||||
import { MemoryService } from '../../../graph_memory/service/memory_service';
|
||||
|
||||
export class GraphMemoryTool implements Tool {
|
||||
readonly id = 'builtin:graph_memory';
|
||||
readonly name = 'GraphMemory';
|
||||
readonly description = `图记忆工具 - 让 AI 拥有真正的长期记忆能力
|
||||
|
||||
操作:
|
||||
- recall: 检索记忆
|
||||
- commit: 写入记忆
|
||||
- purge: 删除记忆
|
||||
- introspect: 查看状态
|
||||
- persona_update/clear: 人设管理
|
||||
- task_create/set_state/delete: 任务管理`;
|
||||
|
||||
readonly category = 'analysis';
|
||||
readonly permissionLevel: 'safe' = 'safe';
|
||||
readonly inputSchema: ToolInputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['recall', 'commit', 'purge', 'introspect', 'persona_update', 'persona_clear',
|
||||
'task_create', 'task_set_state', 'task_delete', 'task_link_info'],
|
||||
description: '记忆操作类型'
|
||||
},
|
||||
params: { type: 'object', description: '操作参数' }
|
||||
},
|
||||
required: ['action', 'params']
|
||||
};
|
||||
|
||||
private db: GraphDatabase;
|
||||
private service: MemoryService;
|
||||
|
||||
constructor(config: { dbPath: string; sessionId?: string }) {
|
||||
this.db = new GraphDatabase(config.dbPath);
|
||||
this.service = new MemoryService(this.db, config.sessionId);
|
||||
}
|
||||
|
||||
async handler(params: Record<string, unknown>, context: ToolExecutionContext): Promise<string> {
|
||||
const action = params.action as string;
|
||||
const actionParams = params.params as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
const result = await this.executeAction(action, actionParams);
|
||||
return JSON.stringify({ success: true, data: result }, null, 2);
|
||||
} catch (error) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: { type: 'execution_error', message: error instanceof Error ? error.message : String(error) }
|
||||
}, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(action: string, params: Record<string, unknown>): Promise<unknown> {
|
||||
switch (action) {
|
||||
case 'recall': return this.service.recall(params as any);
|
||||
case 'commit': return this.service.commit(params as any);
|
||||
case 'purge': return this.service.purge(params as any);
|
||||
case 'introspect': return this.service.introspect();
|
||||
case 'persona_update': return this.service.updatePersona(params);
|
||||
case 'persona_clear': return this.service.clearPersona(params);
|
||||
case 'task_create': return this.service.createTask(params);
|
||||
case 'task_set_state': return this.service.setTaskState(params);
|
||||
case 'task_delete': return this.service.deleteTask(params);
|
||||
default: throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
close(): void { this.db.close(); }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 MemoryService 实现
|
||||
|
||||
```typescript
|
||||
// src/runtime/core/graph_memory/service/memory_service.ts
|
||||
|
||||
import { GraphDatabase } from '../database/graph_database';
|
||||
import type { RecallParams, CommitParams, PurgeParams } from '../types';
|
||||
|
||||
export class MemoryService {
|
||||
private db: GraphDatabase;
|
||||
private sessionId: string;
|
||||
|
||||
constructor(db: GraphDatabase, sessionId?: string) {
|
||||
this.db = db;
|
||||
this.sessionId = sessionId || `session-${Date.now()}`;
|
||||
}
|
||||
|
||||
async recall(params: RecallParams) {
|
||||
return this.db.recall({ ...params, sessionFilter: params.sessionFilter || this.sessionId });
|
||||
}
|
||||
|
||||
async commit(params: CommitParams) {
|
||||
return this.db.commit({ ...params, sessionId: params.sessionId || this.sessionId });
|
||||
}
|
||||
|
||||
async purge(params: PurgeParams) {
|
||||
return this.db.purge(params);
|
||||
}
|
||||
|
||||
async introspect() {
|
||||
const stats = await this.db.introspect();
|
||||
return { ...stats, sessionId: this.sessionId };
|
||||
}
|
||||
|
||||
async updatePersona(params: Record<string, unknown>) {
|
||||
const attributes = params.attributes as Array<{ attribute: string; value: string }>;
|
||||
const mode = params.mode as string || 'merge';
|
||||
|
||||
if (mode === 'replace') {
|
||||
await this.db.purge({ criteria: { subject: 'AI' }, mode: 'soft' });
|
||||
}
|
||||
|
||||
const triplets = attributes.map(attr => ({
|
||||
subject: 'AI', relation: attr.attribute, object: attr.value, confidence: 1.0
|
||||
}));
|
||||
|
||||
await this.commit({ triplets });
|
||||
return { status: 'success', updatedAttributes: attributes.length };
|
||||
}
|
||||
|
||||
async clearPersona(params: Record<string, unknown>) {
|
||||
if (params.confirm === false) return { status: 'cancelled', deletedCount: 0 };
|
||||
const result = await this.purge({ criteria: { subject: 'AI' }, mode: 'soft' });
|
||||
return { status: 'success', deletedCount: result.deleted };
|
||||
}
|
||||
|
||||
async createTask(params: Record<string, unknown>) {
|
||||
const taskId = params.task_id as string;
|
||||
const description = params.description as string;
|
||||
const infoNodes = (params.info_nodes as string[]) || [];
|
||||
|
||||
await this.commit({
|
||||
triplets: [
|
||||
{ subject: taskId, relation: 'is_type', object: 'TaskNode' },
|
||||
{ subject: taskId, relation: 'has_description', object: description },
|
||||
{ subject: taskId, relation: 'HAS_STATE', object: 'State_进行中' }
|
||||
]
|
||||
});
|
||||
|
||||
if (infoNodes.length > 0) {
|
||||
await this.commit({
|
||||
triplets: infoNodes.map(node => ({ subject: taskId, relation: 'CONTAINS_INFO', object: node }))
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'success', taskId };
|
||||
}
|
||||
|
||||
async setTaskState(params: Record<string, unknown>) {
|
||||
const taskId = params.task_id as string;
|
||||
const state = params.state as string;
|
||||
|
||||
await this.purge({ criteria: { subject: taskId, relation: 'HAS_STATE' }, mode: 'soft' });
|
||||
await this.commit({ triplets: [{ subject: taskId, relation: 'HAS_STATE', object: `State_${state}` }] });
|
||||
|
||||
return { status: 'success', newState: state };
|
||||
}
|
||||
|
||||
async deleteTask(params: Record<string, unknown>) {
|
||||
const taskId = params.task_id as string;
|
||||
await this.purge({ criteria: { subject: taskId }, mode: 'soft' });
|
||||
return { status: 'success', taskId };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、测试方案
|
||||
|
||||
### 6.1 测试文件结构
|
||||
|
||||
```
|
||||
WaterFlow/ts/tests/
|
||||
├── runtime/core/graph_memory/
|
||||
│ ├── database/
|
||||
│ │ └── graph_database.test.ts # 15+ 测试
|
||||
│ └── service/
|
||||
│ └── memory_service.test.ts # 12+ 测试
|
||||
└── runtime/core/tools/builtin/
|
||||
└── graph_memory/
|
||||
└── graph_memory_tool.test.ts # 15+ 测试
|
||||
```
|
||||
|
||||
### 6.2 数据库测试
|
||||
|
||||
```typescript
|
||||
// tests/runtime/core/graph_memory/database/graph_database.test.ts
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { GraphDatabase } from '../../../../../src/runtime/core/graph_memory/database/graph_database';
|
||||
import * as fs from 'fs';
|
||||
|
||||
describe('GraphDatabase', () => {
|
||||
const testDbPath = '/tmp/test_graph_memory.db';
|
||||
let db: GraphDatabase;
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
|
||||
db = new GraphDatabase(testDbPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should create entities and relations', async () => {
|
||||
const result = await db.commit({
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: 'Python' },
|
||||
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
|
||||
]
|
||||
});
|
||||
expect(result.createdEntities).toBe(3);
|
||||
expect(result.createdRelations).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recall', () => {
|
||||
beforeEach(async () => {
|
||||
await db.commit({
|
||||
triplets: [
|
||||
{ subject: '用户', relation: '喜欢', object: 'Python' },
|
||||
{ subject: 'Python', relation: '是', object: '编程语言' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should recall by keyword', async () => {
|
||||
const result = await db.recall({ queryIntent: 'Python' });
|
||||
expect(result.entities.some(e => e.name === 'Python')).toBe(true);
|
||||
});
|
||||
|
||||
it('should recall relations', async () => {
|
||||
const result = await db.recall({ queryIntent: '用户,Python' });
|
||||
expect(result.relations.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('purge', () => {
|
||||
beforeEach(async () => {
|
||||
await db.commit({
|
||||
triplets: [{ subject: '旧信息', relation: 'is', object: '垃圾' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('should soft delete relations', async () => {
|
||||
const result = await db.purge({ criteria: { subject: '旧信息' }, mode: 'soft' });
|
||||
expect(result.deleted).toBeGreaterThan(0);
|
||||
expect(result.mode).toBe('soft');
|
||||
});
|
||||
});
|
||||
|
||||
describe('introspect', () => {
|
||||
it('should return statistics', async () => {
|
||||
await db.commit({ triplets: [{ subject: 'A', relation: 'relates', object: 'B' }] });
|
||||
const stats = await db.introspect();
|
||||
expect(stats.entityCount).toBe(2);
|
||||
expect(stats.relationCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 6.3 服务层测试
|
||||
|
||||
```typescript
|
||||
// tests/runtime/core/graph_memory/service/memory_service.test.ts
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { MemoryService } from '../../../../../src/runtime/core/graph_memory/service/memory_service';
|
||||
import { GraphDatabase } from '../../../../../src/runtime/core/graph_memory/database/graph_database';
|
||||
|
||||
describe('MemoryService', () => {
|
||||
const testDbPath = '/tmp/test_memory_service.db';
|
||||
let db: GraphDatabase;
|
||||
let service: MemoryService;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new GraphDatabase(testDbPath);
|
||||
service = new MemoryService(db, 'test-session');
|
||||
});
|
||||
|
||||
describe('persona management', () => {
|
||||
it('should update persona', async () => {
|
||||
const result = await service.updatePersona({
|
||||
attributes: [{ attribute: '角色', value: '猫娘' }],
|
||||
mode: 'replace'
|
||||
});
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.updatedAttributes).toBe(1);
|
||||
});
|
||||
|
||||
it('should clear persona', async () => {
|
||||
await service.updatePersona({ attributes: [{ attribute: '角色', value: '猫娘' }] });
|
||||
const result = await service.clearPersona({ confirm: true });
|
||||
expect(result.status).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('task management', () => {
|
||||
it('should create task', async () => {
|
||||
const result = await service.createTask({
|
||||
task_id: 'Task_Test',
|
||||
description: '测试任务',
|
||||
info_nodes: ['info1']
|
||||
});
|
||||
expect(result.taskId).toBe('Task_Test');
|
||||
});
|
||||
|
||||
it('should set task state', async () => {
|
||||
await service.createTask({ task_id: 'Task_State', description: '测试' });
|
||||
const result = await service.setTaskState({ task_id: 'Task_State', state: '已完成' });
|
||||
expect(result.newState).toBe('已完成');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 6.4 Tool 接口测试
|
||||
|
||||
```typescript
|
||||
// tests/runtime/core/tools/builtin/graph_memory/graph_memory_tool.test.ts
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GraphMemoryTool } from '../../../../../src/runtime/core/tools/builtin/graph_memory/graph_memory_tool';
|
||||
import * as fs from 'fs';
|
||||
|
||||
describe('GraphMemoryTool', () => {
|
||||
const testDbPath = '/tmp/test_graph_memory_tool.db';
|
||||
let tool: GraphMemoryTool;
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
|
||||
tool = new GraphMemoryTool({ dbPath: testDbPath, sessionId: 'test' });
|
||||
});
|
||||
|
||||
it('should have correct metadata', () => {
|
||||
expect(tool.id).toBe('builtin:graph_memory');
|
||||
expect(tool.name).toBe('GraphMemory');
|
||||
expect(tool.category).toBe('analysis');
|
||||
});
|
||||
|
||||
describe('recall', () => {
|
||||
it('should execute recall', async () => {
|
||||
await tool.handler({ action: 'commit', params: { triplets: [{ subject: 'Test', relation: 't', object: 'D' }] } }, mockContext());
|
||||
const result = await tool.handler({ action: 'recall', params: { query_intent: 'Test' } }, mockContext());
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should execute commit', async () => {
|
||||
const result = await tool.handler({
|
||||
action: 'commit',
|
||||
params: { triplets: [{ subject: '用户', relation: '喜欢', object: 'AI' }] }
|
||||
}, mockContext());
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should return error for unknown action', async () => {
|
||||
const result = await tool.handler({ action: 'unknown', params: {} }, mockContext());
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockContext() {
|
||||
return {
|
||||
toolCallId: 'test', workingDirectory: '/tmp', abortController: { signal: {} },
|
||||
config: { timeout: 5000 }, logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 验证检查清单
|
||||
|
||||
```
|
||||
[ ] GraphDatabase.commit - 创建实体和关系
|
||||
[ ] GraphDatabase.recall - 按关键词检索
|
||||
[ ] GraphDatabase.purge - 软删除
|
||||
[ ] GraphDatabase.introspect - 返回统计
|
||||
|
||||
[ ] MemoryService.updatePersona - 人设更新
|
||||
[ ] MemoryService.clearPersona - 人设清除
|
||||
[ ] MemoryService.createTask - 创建任务
|
||||
[ ] MemoryService.setTaskState - 设置状态
|
||||
[ ] MemoryService.deleteTask - 删除任务
|
||||
|
||||
[ ] GraphMemoryTool recall action
|
||||
[ ] GraphMemoryTool commit action
|
||||
[ ] GraphMemoryTool persona_update action
|
||||
[ ] GraphMemoryTool task_create action
|
||||
[ ] GraphMemoryTool error handling
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、实现计划
|
||||
|
||||
| Phase | 任务 | 周期 | 测试 |
|
||||
|-------|------|------|------|
|
||||
| 1 | GraphDatabase 实现 | 2-3 天 | 15+ |
|
||||
| 2 | MemoryService 实现 | 1-2 天 | 12+ |
|
||||
| 3 | GraphMemoryTool 实现 | 1-2 天 | 15+ |
|
||||
| 4 | 集成测试 | 1 天 | 8+ |
|
||||
|
||||
**总计**: 5-8 天,50+ 测试用例
|
||||
@ -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;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export { ChatPage } from './src/main/ets/pages/ChatPage';
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/features/chat
|
||||
@ -1,6 +0,0 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
../../../../common
|
||||
@ -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)')
|
||||
}
|
||||
}
|
||||
@ -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)')
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "chat",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM chat feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export { GraphPage } from './src/main/ets/pages/GraphPage';
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/features/graph
|
||||
@ -1,6 +0,0 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
../../../../common
|
||||
@ -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;
|
||||
}
|
||||
@ -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%')
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "graph",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM graph feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export { SettingsPage } from './src/main/ets/pages/SettingsPage';
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
../../../../common
|
||||
@ -1 +0,0 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/features/settings
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "settings",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM settings feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
221
full_output.log
221
full_output.log
@ -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'
|
||||
@ -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"
|
||||
@ -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*/
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import { appTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: appTasks,
|
||||
plugins: []
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
hwsdk.dir=/home/program/tools/command-line-tools/sdk/default
|
||||
sdk.dir=/home/program/tools/command-line-tools/sdk/default
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
## 1.0.0
|
||||
- 修复once断言问题
|
||||
## 1.0.0-rc
|
||||
- 提供DevEco Studio预览器场景使能的MockSetup装饰器
|
||||
@ -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
|
||||
@ -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) ,请自由地享受和参与开源。
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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;
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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'
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 };
|
||||
@ -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
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
代码告警整改
|
||||
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user