E5.1 Obsidian 插件: CORS中间件 + 记忆面板 + 图谱视图 + 搜索模态框
- 新增 CORS 中间件 (middleware/cors.go): 支持 app://obsidian.md 跨域 - 新增 Obsidian 插件: MemoryView(分页记忆列表) + GraphView(D3力导向图) + SearchModal(语义搜索) - Go API CORS: Access-Control-Allow-Origin: app://obsidian.md - Makefile 新增 build-obsidian / install-obsidian 目标 - 插件安装至 ~/.obsidian/plugins/zhiyi-memory/
This commit is contained in:
parent
f93da87a97
commit
abef38cb05
|
|
@ -30,3 +30,4 @@ __pycache__/
|
|||
.venv/
|
||||
backups/
|
||||
go/build.sh
|
||||
plugins/obsidian/node_modules/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 织忆五步实施计划
|
||||
> 创建时间:2026-05-30
|
||||
> 状态:E1/E2/E3/E4 已完成,E5 规划中
|
||||
> 状态:E1/E2/E3/E4 已完成,E5 规划完成,准备实施
|
||||
> 禁止:偷懒、随意更改变动设计语言
|
||||
|
||||
---
|
||||
|
|
@ -206,22 +206,242 @@ if len(graphDegree) > 0 && graphDegree[0] > 5 {
|
|||
|
||||
---
|
||||
|
||||
## E5:产品 UI(长期)
|
||||
## E5:产品 UI
|
||||
|
||||
### 目标
|
||||
给织忆做一个简单的可视化界面,用于查看记忆、图谱、搜索结果。
|
||||
> 规划版本:v1.0 | 2026-06-02
|
||||
> 目标:给织忆构建三层可视化入口(Obsidian 插件 / 增强 CLI / Web UI),覆盖日常快速查询和图谱深度探索两种场景。
|
||||
|
||||
### 现状
|
||||
- 只能 API 调,没有界面
|
||||
- 个人用足够,但不方便查看图谱结构
|
||||
---
|
||||
|
||||
### 实施步骤
|
||||
待定,优先级最低。前 4 个阶段完成后再规划。
|
||||
### E5.1:Obsidian 插件(优先级最高)
|
||||
|
||||
### 可能的方案
|
||||
- 简单 Web UI(React + Go API)
|
||||
- Obsidian 插件直接可视化
|
||||
- CLI 增强(tree/graph 可视化)
|
||||
**为什么先做 Obsidian**
|
||||
- 牧尘的笔记和记忆本来就在 Obsidian 里,界面切换成本最低
|
||||
- 插件形式天然接入 vault 工作流,不需要另外打开窗口
|
||||
- 图谱可以直接嵌入笔记界面,实体关系和笔记内容联动
|
||||
|
||||
**目标功能**
|
||||
- [ ] E5.1.1 插件骨架:`manifest.json` + `main.ts` + `styles.css`,Obsidian 加载并注册 `ZhiYiPlugin`
|
||||
- [ ] E5.1.2 记忆侧边栏面板:展示最近记忆列表,支持按 namespace 过滤,支持分页
|
||||
- [ ] E5.1.3 实体图谱视图:基于 D3.js force-directed graph,从 `/api/v1/graph/entity/{entity}/neighbors` 获取数据,节点颜色区分 category,hover 显示关系标签
|
||||
- [ ] E5.1.4 记忆搜索模态框:输入查询词调用 `/api/v1/search/recall`,显示 top-20 结果,点击跳转到记忆详情
|
||||
- [ ] E5.1.5 实体详情视图:选中图谱节点后,从 `/api/v1/memories/by-entity/{entity}` 获取关联记忆列表
|
||||
- [ ] E5.1.6 蒸馏状态面板:展示 `/api/v1/distill/status` 和 `/api/v1/distill/quota`,队列为非空时高亮提醒
|
||||
|
||||
**技术方案**
|
||||
- 开发目录:`~/projects/memoryweave/plugins/obsidian/`
|
||||
- 插件通过 `fetch()` 调用 Go API(端口 7821),Go 服务需添加 CORS 头(`Access-Control-Allow-Origin: app://obsidian.md`)
|
||||
- 图谱渲染:D3.js v7 从 CDN 加载(`https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js`),不用本地打包
|
||||
- 构建:esbuild 打包 `main.ts` → `main.js`(`npx esbuild main.ts --bundle --outfile=main.js`)
|
||||
- Obsidian 开启「第三方插件」后,插件文件夹挂载到 `~/.obsidian/plugins/zhiyi-memory/`
|
||||
|
||||
**CORS 适配(Go 服务改动)**
|
||||
```go
|
||||
// api/middleware/cors.go — 新增
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "app://obsidian.md")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "X-API-Key, Content-Type")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
// server.go — 注册 middleware
|
||||
server.Use(apiMiddleware.CORS())
|
||||
```
|
||||
|
||||
**文件结构**
|
||||
```
|
||||
plugins/obsidian/
|
||||
├── manifest.json # Obsidian 插件清单
|
||||
├── styles.css # 插件样式
|
||||
├── main.ts # 插件入口,注册侧栏、图谱视图、搜索模态框
|
||||
├── src/
|
||||
│ ├── api.ts # 调用 Go API(fetch 封装,baseURL = http://localhost:7821)
|
||||
│ ├── MemoryView.ts # 记忆侧边栏面板
|
||||
│ ├── GraphView.ts # D3 图谱渲染
|
||||
│ └── SearchModal.ts # 搜索弹窗
|
||||
├── esbuild.config.mjs # 构建配置
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**验收标准**
|
||||
- [ ] Obsidian 加载插件后,左侧出现「织忆」侧边栏
|
||||
- [ ] 侧边栏显示最近 20 条记忆(namespace=default),点击展开内容
|
||||
- [ ] 图谱视图能渲染至少 3 层邻居节点,节点可拖拽
|
||||
- [ ] 搜索模态框输入关键词返回结果(<500ms)
|
||||
- [ ] 蒸馏队列非空时侧边栏顶部出现红色提示
|
||||
|
||||
---
|
||||
|
||||
### E5.2:增强 CLI(第二优先级)
|
||||
|
||||
**目标功能**
|
||||
- [ ] E5.2.1 `zhiyi tree` 命令:树形展示 namespace 下记忆结构,按 category 分组,每条记忆显示前 60 字符摘要
|
||||
- [ ] E5.2.2 `zhiyi graph` 命令:ASCII art 渲染 ego-network 图谱(中心节点 + 一跳邻居 + 关系标签)
|
||||
- [ ] E5.2.3 `zhiyi recall <query>` 命令:语义搜索,返回 top-10 结果,显示 relevance score 和摘要
|
||||
- [ ] E5.2.4 `zhiyi stats` 命令:显示记忆总数、namespace 分布、今日新增、蒸馏队列状态
|
||||
- [ ] E5.2.5 `zhiyi entity <name>` 命令:查询实体详情(出现次数、关联实体列表、记忆片段)
|
||||
|
||||
**技术方案**
|
||||
- CLI 命令入口:`~/projects/memoryweave/go/cmd/zhiyi-cli/`
|
||||
- 使用 `cobra` 或原生 `flag` 解析子命令
|
||||
- 图谱 ASCII 渲染:用 Unicode box-drawing 字符(`┌─┬┐│├┼┤└┴┘`),中心节点用 `◉`,邻居用 `○`
|
||||
- 调用现有 Go API 端点,不直接操作存储
|
||||
|
||||
**文件结构**
|
||||
```
|
||||
go/cmd/zhiyi-cli/
|
||||
├── main.go
|
||||
├── cmd/
|
||||
│ ├── root.go
|
||||
│ ├── tree.go
|
||||
│ ├── graph.go
|
||||
│ ├── recall.go
|
||||
│ ├── stats.go
|
||||
│ └── entity.go
|
||||
└── output/
|
||||
├── ascii_graph.go # ASCII 图谱渲染
|
||||
└── formatter.go # 格式化输出
|
||||
```
|
||||
|
||||
**验收标准**
|
||||
- [ ] `zhiyi tree` 输出格式正确(树形、分组、摘要)
|
||||
- [ ] `zhiyi graph <entity>` 渲染 ASCII 图谱,实体数 ≥ 5 时换行正确
|
||||
- [ ] `zhiyi recall` 输出 relevance score 排序正确
|
||||
- [ ] `zhiyi stats` 显示记忆数、namespace 分布、蒸馏配额(used/limit)
|
||||
|
||||
---
|
||||
|
||||
### E5.3:Web UI(第三优先级)
|
||||
|
||||
**目标功能**
|
||||
- [ ] E5.3.1 React 项目骨架:Vite + React + TypeScript,路由 `/memories` `/graph` `/search` `/distill`
|
||||
- [ ] E5.3.2 记忆列表页:分页表格(每页 20 条),列:id / content_preview / category / created_at / namespace,支持点击展开完整内容
|
||||
- [ ] E5.3.3 图谱探索页:全屏 D3.js force-directed graph,支持缩放/拖拽/筛选(category / namespace),点击节点弹出详情 drawer
|
||||
- [ ] E5.3.4 语义搜索页:输入框 + 实时结果(debounce 300ms),显示 relevance 和摘要,高亮匹配片段
|
||||
- [ ] E5.3.5 蒸馏监控页:进度条显示 daily used / limit,队列列表(episode_id / category / content_preview)
|
||||
- [ ] E5.3.6 响应式布局,支持 1280px+ 宽屏
|
||||
|
||||
**技术方案**
|
||||
- 项目目录:`~/projects/memoryweave/web-ui/`
|
||||
- 技术栈:Vite + React 18 + TypeScript + TailwindCSS + D3.js v7
|
||||
- API 层:Axios 调用 Go API,响应式状态用 React Query 管理缓存
|
||||
- 图谱:与 E5.1 共用 `/api/v1/graph/navigate` 和邻居接口,数据结构一致
|
||||
- 部署:Go 服务新增静态文件中间件(`/static/*` → `web-ui/dist/`),`make build-web` 构建后自动同步
|
||||
|
||||
**文件结构**
|
||||
```
|
||||
web-ui/
|
||||
├── index.html
|
||||
├── package.json
|
||||
├── vite.config.ts
|
||||
├── tailwind.config.js
|
||||
├── src/
|
||||
│ ├── main.tsx
|
||||
│ ├── App.tsx
|
||||
│ ├── api/
|
||||
│ │ └── zhiyi.ts # API 客户端封装
|
||||
│ ├── pages/
|
||||
│ │ ├── MemoriesPage.tsx
|
||||
│ │ ├── GraphPage.tsx
|
||||
│ │ ├── SearchPage.tsx
|
||||
│ │ └── DistillPage.tsx
|
||||
│ └── components/
|
||||
│ ├── GraphCanvas.tsx # D3 图谱组件
|
||||
│ ├── MemoryTable.tsx
|
||||
│ └── DistillStatus.tsx
|
||||
└── dist/ # 构建输出,由 Go 静态中间件托管
|
||||
```
|
||||
|
||||
**Go 服务静态文件中间件**
|
||||
```go
|
||||
// api/middleware/static.go — 新增
|
||||
func StaticFile(root string) gin.HandlerFunc {
|
||||
fs := http.FileServer(http.Dir(root))
|
||||
return func(c *gin.Context) {
|
||||
if _, err := os.Stat(filepath.Join(root, c.Request.URL.Path)); err == nil {
|
||||
fs.ServeHTTP(c.Writer, c.Request)
|
||||
c.Abort()
|
||||
} else {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
// server.go — 注册
|
||||
if opt.Mode == "dev" {
|
||||
server.Use(apiMiddleware.StaticFile("../web-ui/dist"))
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**
|
||||
- [ ] Web UI 能加载并显示记忆列表(分页正常)
|
||||
- [ ] 图谱页渲染实体节点 ≥ 10 个,缩放拖拽流畅
|
||||
- [ ] 搜索页输入关键词后 1 秒内显示结果,高亮匹配文字
|
||||
- [ ] 蒸馏监控页显示正确的 used/limit 进度条
|
||||
- [ ] 各页面在 1920×1080 和 1366×768 下布局正常
|
||||
|
||||
---
|
||||
|
||||
### E5 总体依赖关系
|
||||
|
||||
```
|
||||
E5.1 (Obsidian 插件)
|
||||
└── Go API 需添加 CORS 中间件
|
||||
└── 构建系统需新增 esbuild 步骤
|
||||
|
||||
E5.2 (增强 CLI)
|
||||
└── 复用 E5.1 的 CORS 无关紧要
|
||||
└── 直接调用 Go API(无需其他依赖)
|
||||
|
||||
E5.3 (Web UI)
|
||||
└── 复用 E5.1 的 CORS 中间件
|
||||
└── Go 服务新增静态文件中间件
|
||||
└── 需要独立的 Vite 构建流程
|
||||
```
|
||||
|
||||
### 实施顺序
|
||||
|
||||
**第一波(E5.1 Obsidian 插件)**
|
||||
1. 添加 Go CORS 中间件,构建部署
|
||||
2. 创建 `plugins/obsidian/` 目录结构
|
||||
3. 实现 `ZhiYiPlugin` 骨架,注册侧边栏
|
||||
4. 实现 MemoryView(记忆列表)
|
||||
5. 实现 GraphView(D3 图谱)
|
||||
6. 实现 SearchModal(搜索)
|
||||
7. 本地测试:Obsidian 加载插件,验证全部功能
|
||||
8. 提交,WORKLOG 同步
|
||||
|
||||
**第二波(E5.2 增强 CLI)**
|
||||
1. 创建 `go/cmd/zhiyi-cli/` 项目结构
|
||||
2. 实现 tree / graph / recall / stats / entity 命令
|
||||
3. 本地测试所有子命令
|
||||
4. 提交,WORKLOG 同步
|
||||
|
||||
**第三波(E5.3 Web UI)**
|
||||
1. 初始化 Vite + React + TypeScript 项目
|
||||
2. 实现 MemoriesPage
|
||||
3. 实现 GraphPage(基于 E5.1 相同的 D3 数据源)
|
||||
4. 实现 SearchPage
|
||||
5. 实现 DistillPage
|
||||
6. Go 服务添加静态文件中间件
|
||||
7. `make build-web` 集成到 Makefile
|
||||
8. 完整测试,提交
|
||||
|
||||
### 附录:外部调研(GitHub 开源参考)
|
||||
|
||||
| 方向 | 参考项目 | 关键技术 |
|
||||
|------|---------|---------|
|
||||
| Obsidian 插件 | `obsidianmd/obsidian-sample-plugin` | manifest.json, Plugin class, CustomView |
|
||||
| 图谱可视化 | `react-force-graph` (底层 D3) | force-directed layout, zoom/pan |
|
||||
| Web UI 图谱 | `vis-network` / `react-vis` | alternative to raw D3 |
|
||||
| CLI 图谱 | `dogmap`(Mastodon ASCII 工具) | box-drawing 字符布局 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -252,8 +472,8 @@ if len(graphDegree) > 0 && graphDegree[0] > 5 {
|
|||
| E2 多 agent 命名空间 | 2026-05-30 | 2026-05-30 | ✅ |
|
||||
| E3 增量 embedding | 2026-05-30 | 2026-05-30 | ✅ |
|
||||
| E4 图谱推理 | 2026-05-31 | 2026-05-31 | ✅ E4.1/E4.2/E4.3 已实现,E4.3 调用方已接入(extractTopEntityDegree)
|
||||
| E5 产品 UI | - | - | ⏳ |
|
||||
| E5 产品 UI | 2026-06-02 | - | 🔨 | E5.1/E5.2/E5.3 规划完成,按序实施 |
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-05-31*
|
||||
*最后更新:2026-06-02(E5 规划完成)*
|
||||
12
Makefile
12
Makefile
|
|
@ -23,6 +23,18 @@ build-rust:
|
|||
|
||||
build: build-go ## 只构建 Go (Rust 需单独 build-rust)
|
||||
|
||||
# ─── Obsidian 插件 ────────────────────────────────────
|
||||
OBSIDIAN_PLUGIN := plugins/obsidian
|
||||
OBSIDIAN_DEST := $(HOME)/.obsidian/plugins/zhiyi-memory
|
||||
|
||||
build-obsidian:
|
||||
cd $(OBSIDIAN_PLUGIN) && node esbuild.config.mjs
|
||||
|
||||
install-obsidian: build-obsidian
|
||||
cp $(OBSIDIAN_PLUGIN)/main.js $(OBSIDIAN_DEST)/
|
||||
cp $(OBSIDIAN_PLUGIN)/manifest.json $(OBSIDIAN_DEST)/
|
||||
cp $(OBSIDIAN_PLUGIN)/styles.css $(OBSIDIAN_DEST)/
|
||||
|
||||
# ─── 测试 ────────────────────────────────────────────
|
||||
|
||||
test:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
// CORS 中间件 — 支持 Obsidian 插件从 app://obsidian.md 调用 Go API
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const obsidianOrigin = "app://obsidian.md"
|
||||
|
||||
// CORS 返回支持 Obsidian 的跨域中间件
|
||||
func CORS() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", obsidianOrigin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "X-API-Key, Content-Type, Authorization")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1065,7 +1065,7 @@ func NewServer() http.Handler {
|
|||
})
|
||||
})
|
||||
|
||||
return middleware.Auth(mux)
|
||||
return middleware.CORS()(middleware.Auth(mux))
|
||||
}
|
||||
|
||||
// ─── 辅助 ──────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
# 织忆 Obsidian 插件
|
||||
|
||||
> 给织忆记忆系统提供 Obsidian 内置可视化界面。
|
||||
|
||||
## 功能
|
||||
|
||||
- **🧠 记忆面板**:侧边栏展示记忆列表,分页浏览,支持蒸馏队列提醒
|
||||
- **📐 图谱视图**:D3.js force-directed graph,输入实体探索 ego-network,节点可拖拽
|
||||
- **🔍 搜索模态框**:`Ctrl+P` / 命令面板调起,实时语义搜索(debounce 300ms)
|
||||
|
||||
## 安装
|
||||
|
||||
### 方式一:本地开发安装
|
||||
|
||||
1. **构建插件**
|
||||
```bash
|
||||
cd plugins/obsidian
|
||||
npm install
|
||||
node esbuild.config.mjs
|
||||
```
|
||||
|
||||
2. **Obsidian 开启第三方插件**
|
||||
- 设置 → 社区插件 → 开启「第三方插件」
|
||||
- 安全模式会阻止社区插件,按提示关闭
|
||||
|
||||
3. **复制到插件目录**
|
||||
```bash
|
||||
cp -r plugins/obsidian ~/.obsidian/plugins/zhiyi-memory/
|
||||
```
|
||||
|
||||
4. **重载 Obsidian**
|
||||
- 命令面板(`Ctrl+P`)输入 `Reload`
|
||||
- 或重启 Obsidian
|
||||
|
||||
5. **启用插件**
|
||||
- 设置 → 社区插件 → 找到「织忆」→ 启用
|
||||
|
||||
### 方式二:开发者热重载
|
||||
|
||||
```bash
|
||||
# 进入插件目录,watch 模式
|
||||
node esbuild.config.mjs --watch
|
||||
|
||||
# 修改后自动重新构建,Obsidian 中按 Ctrl+P → Reload vault
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
### 命令面板(`Ctrl+P`)
|
||||
|
||||
- `织忆:打开织忆记忆面板` — 右侧边栏记忆列表
|
||||
- `织忆:打开织忆图谱面板` — 图谱探索视图
|
||||
- `织忆:搜索织忆记忆` — 语义搜索弹窗
|
||||
|
||||
### 图谱操作
|
||||
|
||||
- **输入实体名** → 回车 → 加载该实体的 ego-network(中心节点 + 一跳邻居)
|
||||
- **点击节点** → 展开邻居(递归探索)
|
||||
- **滚轮缩放** / **拖拽平移** / **节点拖动重排**
|
||||
- **点击「Top 节点」** → 自动加载 PageRank 最高的实体
|
||||
|
||||
### 蒸馏状态
|
||||
|
||||
- 记忆面板顶部蒸馏队列 > 0 时自动出现红色警告条
|
||||
|
||||
## API 依赖
|
||||
|
||||
插件调用 `http://localhost:7821` 的织忆 Go API,确保服务运行:
|
||||
|
||||
```bash
|
||||
systemctl --user start zhiyid
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
plugins/obsidian/
|
||||
├── manifest.json # Obsidian 插件清单
|
||||
├── styles.css # 全局样式
|
||||
├── main.ts # 插件入口(源)
|
||||
├── main.js # esbuild 构建产物(Obsidian 加载这个)
|
||||
├── src/
|
||||
│ ├── api.ts # Go API 客户端
|
||||
│ ├── MemoryView.ts # 记忆侧边栏面板
|
||||
│ ├── GraphView.ts # D3 图谱渲染
|
||||
│ └── SearchModal.ts # 搜索弹窗
|
||||
├── esbuild.config.mjs # 构建配置
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
- D3.js v7 从 CDN(`cdn.jsdelivr.net/npm/d3@7`)动态加载,不打包
|
||||
- Obsidian 插件 API 类型由 Obsidian host 在运行时提供
|
||||
- CORS 由 Go API 中间件处理(`Access-Control-Allow-Origin: app://obsidian.md`)
|
||||
- 构建产物 `main.js` 约 200KB(minified,gzip 后更小)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// esbuild config — 打包 main.ts → main.js
|
||||
import * as esbuild from "esbuild";
|
||||
|
||||
const isWatch = process.argv.includes("--watch");
|
||||
|
||||
const buildOptions = {
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
mainFields: ["browser", "module", "main"],
|
||||
platform: "browser",
|
||||
target: "es2020",
|
||||
outfile: "main.js",
|
||||
format: "cjs",
|
||||
sourcemap: false,
|
||||
minify: !isWatch,
|
||||
external: ["obsidian"],
|
||||
define: {
|
||||
"process.env.NODE_ENV": isWatch ? '"development"' : '"production"',
|
||||
},
|
||||
};
|
||||
|
||||
if (isWatch) {
|
||||
const ctx = await esbuild.context(buildOptions);
|
||||
await ctx.watch();
|
||||
console.log("Watching for changes...");
|
||||
} else {
|
||||
await esbuild.build(buildOptions);
|
||||
console.log("Build complete: main.js");
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
var E=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var j=(a,r,e)=>r in a?E(a,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[r]=e;var Q=(a,r)=>{for(var e in r)E(a,e,{get:r[e],enumerable:!0})},K=(a,r,e,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of W(r))!Y.call(a,n)&&n!==e&&E(a,n,{get:()=>r[n],enumerable:!(i=O(r,n))||i.enumerable});return a};var U=a=>K(E({},"__esModule",{value:!0}),a);var s=(a,r,e)=>j(a,typeof r!="symbol"?r+"":r,e);var Z={};Q(Z,{default:()=>M});module.exports=U(Z);var m=require("obsidian");var I=require("obsidian");var X="http://localhost:7821",F="zhiyi-dev-key-2026";async function x(a,r={}){let n={method:r.method??"GET",headers:{"X-API-Key":F,"Content-Type":"application/json"}};r.body!==void 0&&(n.body=JSON.stringify(r.body));let o=await fetch(`${X}${a}`,n);if(!o.ok)throw new Error(`API ${a} failed: ${o.status} ${o.statusText}`);let l=await o.text();return l?JSON.parse(l):{}}async function P(a){let r=new URLSearchParams;a.namespace&&r.set("namespace",a.namespace),a.limit&&r.set("limit",String(a.limit)),a.offset!==void 0&&r.set("offset",String(a.offset));let e=r.toString();return x(`/api/v1/memories?${e}`)}async function R(a,r="hermes-main"){return x("/api/v1/search/recall",{method:"POST",body:{query:a,namespace:r,top_k:20}})}async function _(a){return x(`/api/v1/graph/navigate?entity=${encodeURIComponent(a)}&depth=1`)}async function N(a="hermes-main",r=100){return x(`/api/v1/graph/export?namespace=${a}&limit=${r}`)}async function H(){return x("/api/v1/distill/status")}async function V(){return x("/api/v1/distill/quota")}async function C(a=30){return x(`/api/v1/graph/pagerank?limit=${a}`)}async function $(a){return x(`/api/v1/memories/by-entity/${encodeURIComponent(a)}`)}var b="zhiyi-memory-view",k=class extends I.ItemView{constructor(e){super(e);s(this,"container");s(this,"memories",[]);s(this,"currentPage",0);s(this,"pageSize",20);s(this,"totalMemories",0);s(this,"status_interval",null);this.container=this.contentEl}getViewType(){return b}getDisplayText(){return"\u7EC7\u5FC6\u8BB0\u5FC6"}async onOpen(){this.render(),this.startStatusPoll()}async onClose(){this.status_interval!==null&&window.clearInterval(this.status_interval)}startStatusPoll(){let e=window.setInterval(()=>this.renderDistillBanner(),3e4);this.status_interval=e}async renderDistillBanner(){let e=this.container.querySelector(".zhiyi-distill-banner");if(e)try{let[i,n]=await Promise.all([H(),V()]);i.queue_len>0?(e.innerHTML=`<span class="zhiyi-warn">\u26A0\uFE0F \u84B8\u998F\u961F\u5217: ${i.queue_len} \u6761</span>`,e.style.display="block"):e.style.display="none"}catch{}}async render(){this.container.empty(),this.container.createEl("style",{text:`
|
||||
.zhiyi-memory-view { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
|
||||
.zhiyi-header { padding: 10px 12px 6px; border-bottom: 1px solid var(--background-modifier-border); }
|
||||
.zhiyi-header h1 { font-size: 14px; font-weight: 600; margin: 0 0 4px; }
|
||||
.zhiyi-stats { color: var(--text-muted); font-size: 11px; margin: 0; }
|
||||
.zhiyi-distill-banner {
|
||||
display: none; background: #c44; color: white; padding: 4px 12px;
|
||||
font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
.zhiyi-body { flex: 1; overflow-y: auto; }
|
||||
.zhiyi-item {
|
||||
padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.zhiyi-item:hover { background: var(--background-modifier-hover); }
|
||||
.zhiyi-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; }
|
||||
.zhiyi-item-cat { font-size: 10px; color: var(--text-muted); background: var(--background-secondary); padding: 1px 4px; border-radius: 2px; }
|
||||
.zhiyi-item-score { font-size: 10px; color: var(--text-muted); }
|
||||
.zhiyi-item-content { font-size: 12px; color: var(--text-normal); line-height: 1.4; }
|
||||
.zhiyi-item-time { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
||||
.zhiyi-item-id { font-size: 10px; color: var(--text-faint); font-family: monospace; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.zhiyi-pagination { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--background-modifier-border); }
|
||||
.zhiyi-pagination button { flex: 1; padding: 4px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; font-size: 11px; }
|
||||
.zhiyi-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.zhiyi-loading { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
.zhiyi-empty { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
`}),this.container.createEl("div",{cls:"zhiyi-memory-view"}).append(this.container.createEl("div",{cls:"zhiyi-distill-banner"}),this.container.createEl("div",{cls:"zhiyi-header"}).append(this.container.createEl("h1",{text:"\u{1F9E0} \u7EC7\u5FC6\u8BB0\u5FC6"}),this.container.createEl("p",{cls:"zhiyi-stats",text:"\u52A0\u8F7D\u4E2D..."})),this.container.createEl("div",{cls:"zhiyi-body"}),this.container.createEl("div",{cls:"zhiyi-pagination"})),await this.loadMemories(),await this.renderDistillBanner()}async loadMemories(){let e=this.container.querySelector(".zhiyi-body"),i=this.container.querySelector(".zhiyi-stats");e.innerHTML='<div class="zhiyi-loading">\u52A0\u8F7D\u4E2D\u2026</div>';try{let n=await P({namespace:"hermes-main",limit:this.pageSize,offset:this.currentPage*this.pageSize});this.memories=n.memories||[],this.totalMemories=n.total||0,i.setText(`${this.totalMemories} \u6761\u8BB0\u5FC6 \xB7 \u7B2C ${this.currentPage+1} \u9875`),this.renderList()}catch(n){e.innerHTML=`<div class="zhiyi-empty">\u52A0\u8F7D\u5931\u8D25: ${n.message}</div>`}}renderList(){let e=this.container.querySelector(".zhiyi-body"),i=this.container.querySelector(".zhiyi-pagination");e.empty(),this.memories.length===0&&e.createEl("div",{cls:"zhiyi-empty",text:"\u6682\u65E0\u8BB0\u5FC6"});for(let o of this.memories){let l=e.createEl("div",{cls:"zhiyi-item"}),d=l.createEl("div",{cls:"zhiyi-item-header"});d.createEl("span",{cls:"zhiyi-item-cat",text:o.category||"normal"}),d.createEl("span",{cls:"zhiyi-item-score",text:`score: ${(o.quality_score??0).toFixed(2)}`}),l.createEl("div",{cls:"zhiyi-item-content",text:this.truncate(o.content,120)}),l.createEl("div",{cls:"zhiyi-item-time",text:this.formatTime(o.created_at)}),l.createEl("div",{cls:"zhiyi-item-id",text:o.id})}let n=Math.ceil(this.totalMemories/this.pageSize);if(i.empty(),n>1){let o=i.createEl("button",{text:"\u25C0 \u4E0A\u4E00\u9875"});o.setAttr("disabled",this.currentPage===0?"true":""),o.onclick=()=>{this.currentPage--,this.loadMemories()},i.createEl("span",{text:`${this.currentPage+1}/${n}`,cls:"zhiyi-item-cat"});let l=i.createEl("button",{text:"\u4E0B\u4E00\u9875 \u25B6"});l.setAttr("disabled",this.currentPage>=n-1?"true":""),l.onclick=()=>{this.currentPage++,this.loadMemories()}}}truncate(e,i){return e?e.length>i?e.slice(0,i)+"\u2026":e:""}formatTime(e){if(!e)return"";try{return new Date(e).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return e}}};var D=require("obsidian");async function J(){return window.d3?window.d3:new Promise((a,r)=>{let e=document.createElement("script");e.src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js",e.onload=()=>a(window.d3),e.onerror=r,document.head.appendChild(e)})}var w="zhiyi-graph-view",S=class extends D.ItemView{constructor(e){super(e);s(this,"container");s(this,"currentEntity","");s(this,"d3",null);s(this,"svg",null);s(this,"simulation",null);this.container=this.contentEl}getViewType(){return w}getDisplayText(){return"\u7EC7\u5FC6\u56FE\u8C31"}async onOpen(){this.render()}async onClose(){this.simulation?.stop()}async render(){this.container.empty(),this.container.createEl("style",{text:`
|
||||
.zhiyi-graph-view { height: 100%; display: flex; flex-direction: column; }
|
||||
.zhiyi-graph-toolbar {
|
||||
display: flex; gap: 6px; padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
align-items: center;
|
||||
}
|
||||
.zhiyi-graph-toolbar input {
|
||||
flex: 1; padding: 4px 8px; font-size: 12px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px; color: var(--text-normal);
|
||||
}
|
||||
.zhiyi-graph-toolbar button {
|
||||
padding: 4px 10px; font-size: 12px; cursor: pointer;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.zhiyi-graph-toolbar .info {
|
||||
font-size: 11px; color: var(--text-muted); padding: 0 6px;
|
||||
}
|
||||
.zhiyi-graph-canvas { flex: 1; position: relative; overflow: hidden; }
|
||||
.zhiyi-graph-canvas svg { width: 100%; height: 100%; }
|
||||
.zhiyi-graph-detail {
|
||||
position: absolute; bottom: 0; left: 0; right: 0;
|
||||
background: var(--background-secondary);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding: 8px 12px; font-size: 11px; max-height: 120px; overflow-y: auto;
|
||||
}
|
||||
.zhiyi-graph-detail-title { font-weight: 600; margin-bottom: 4px; }
|
||||
.zhiyi-graph-node-label { font-size: 10px; fill: var(--text-muted); pointer-events: none; }
|
||||
.zhiyi-legend { position: absolute; top: 8px; right: 8px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 10px; }
|
||||
.zhiyi-legend-item { display: flex; align-items: center; gap: 4px; margin: 2px 0; }
|
||||
.zhiyi-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.zhiyi-tooltip {
|
||||
position: absolute; background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px; padding: 6px 8px; font-size: 11px;
|
||||
pointer-events: none; opacity: 0; transition: opacity 0.15s;
|
||||
max-width: 200px; z-index: 10;
|
||||
}
|
||||
`});let e=this.container.createEl("div",{cls:"zhiyi-graph-view"}),i=e.createEl("div",{cls:"zhiyi-graph-toolbar"}),n=i.createEl("input",{attr:{placeholder:"\u8F93\u5165\u5B9E\u4F53\u540D\u79F0\uFF0C\u56DE\u8F66\u67E5\u770B ego-network\uFF08\u5982: \u7267\u5C18\uFF09"}}),o=i.createEl("button",{text:"\u52A0\u8F7D\u56FE\u8C31"});i.createEl("span",{cls:"info",text:"\u70B9\u51FB\u8282\u70B9\u67E5\u770B\u8BE6\u60C5"});let l=i.createEl("button",{text:"Top \u8282\u70B9"}),d=e.createEl("div",{cls:"zhiyi-graph-canvas"}),p=d.createEl("div",{cls:"zhiyi-tooltip"}),u=d.createEl("div",{cls:"zhiyi-legend"});u.innerHTML=`
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#4a9eff"></span> \u9ED8\u8BA4</div>
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#f6c945"></span> \u4EBA\u7269</div>
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#34c759"></span> \u9879\u76EE</div>
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#ff6b6b"></span> \u4E8B\u4EF6</div>
|
||||
`;let y=e.createEl("div",{cls:"zhiyi-graph-detail",attr:{style:"display:none"}}),g=y.createEl("div",{cls:"zhiyi-graph-detail-title"}),f=y.createEl("div",{cls:"zhiyi-graph-detail-content"});this.d3=await J(),this.svg=this.d3.select(d.createEl("svg")),await this.renderGlobalGraph(),o.onclick=async()=>{n.value.trim()&&await this.renderEgoGraph(n.value.trim(),d,p,y,g,f)},n.onkeydown=async h=>{h.key==="Enter"&&n.value.trim()&&await this.renderEgoGraph(n.value.trim(),d,p,y,g,f)},l.onclick=async()=>{try{let z=(await C(20)).pagerank?.[0]?.entity||"\u7267\u5C18";n.value=z,await this.renderEgoGraph(z,d,p,y,g,f)}catch{}}}async renderGlobalGraph(){try{let e=await N("hermes-main",80);await this.renderD3Graph(e.nodes,e.edges,null)}catch{}}async renderEgoGraph(e,i,n,o,l,d){this.currentEntity=e,this.simulation?.stop();try{let[p,u]=await Promise.all([_(e),$(e)]),g=[{id:e,label:e},...p.nodes.filter(h=>h.id!==e)];await this.renderD3Graph(g,p.edges,e),o.style.display="block",l.setText(`\u{1F4CC} ${e}`);let f=u.memories||[];d.innerHTML=f.slice(0,3).map(h=>`<div style="margin:2px 0">\u2022 ${this.truncate(h.content,100)}</div>`).join("")||"<div>\u65E0\u5173\u8054\u8BB0\u5FC6</div>"}catch(p){let u=i.createEl("div",{text:`\u52A0\u8F7D\u5931\u8D25: ${p.message}`,cls:"zhiyi-empty"});setTimeout(()=>u.remove(),3e3)}}async renderD3Graph(e,i,n){if(!this.d3||!this.svg)return;let o=this.d3,l=this.svg;l.selectAll("*").remove();let d=l.node().clientWidth||600,p=l.node().clientHeight||400,u={\u4EBA\u7269:"#f6c945",\u4EBA\u7269_name:"#f6c945",project:"#34c759",\u9879\u76EE:"#34c759",event:"#ff6b6b",\u4E8B\u4EF6:"#ff6b6b",concept:"#a855f7",\u6982\u5FF5:"#a855f7",location:"#06b6d4",distilled:"#4a9eff",default:"#4a9eff"},y=t=>t.id===n?"#ff4d4d":t.category&&u[t.category]?u[t.category]:u[t.weight!==void 0&&t.weight>5?"distilled":"default"],g=e.map(t=>({id:t.id,label:t.label||t.id,category:t.category,weight:t.weight,isCentral:t.id===n})),f=new Map(g.map(t=>[t.id,t])),h=i.filter(t=>f.has(t.source)&&f.has(t.target)).map(t=>({source:t.source,target:t.target,relation:t.relation,weight:t.weight})),z=o.zoom().scaleExtent([.2,5]).on("zoom",t=>{L.attr("transform",t.transform)});l.call(z);let L=l.append("g");l.append("defs").append("marker").attr("id","arrowhead").attr("viewBox","-0 -5 10 10").attr("refX",15).attr("refY",0).attr("orient","auto").attr("markerWidth",6).attr("markerHeight",6).append("path").attr("d","M 0,-5 L 10,0 L 0,5").attr("fill","#666");let q=L.append("g").selectAll("line").data(h).enter().append("line").attr("stroke","#999").attr("stroke-opacity",.6).attr("stroke-width",t=>Math.min(2,t.weight||1)).attr("marker-end","url(#arrowhead)"),v=L.append("g").selectAll("g").data(g).enter().append("g").style("cursor","pointer");v.append("circle").attr("r",t=>t.isCentral?12:6+(t.weight||1)*.5).attr("fill",t=>y(t)).attr("stroke",t=>t.isCentral?"#ff4d4d":"white").attr("stroke-width",t=>t.isCentral?2:1.5),v.append("text").text(t=>t.label.length>12?t.label.slice(0,12)+"\u2026":t.label).attr("dy",-8).attr("text-anchor","middle").attr("class","zhiyi-graph-node-label").style("font-size",t=>t.isCentral?"11px":"9px").style("fill","var(--text-muted)");let B=o.drag().on("start",(t,c)=>{t.active||this.simulation.alphaTarget(.3).restart(),c.fx=c.x,c.fy=c.y}).on("drag",(t,c)=>{c.fx=t.x,c.fy=t.y}).on("end",(t,c)=>{t.active||this.simulation.alphaTarget(0),c.fx=null,c.fy=null});v.call(B),v.on("click",(t,c)=>{this.renderEgoGraph(c.id,canvas.parentElement,tooltip,canvas.parentElement.querySelector(".zhiyi-graph-detail"),canvas.parentElement.querySelector(".zhiyi-graph-detail-title"),canvas.parentElement.querySelector(".zhiyi-graph-detail-content"))}),v.on("mouseover",(t,c)=>{tooltip.style.opacity="1",tooltip.style.left=t.offsetX+10+"px",tooltip.style.top=t.offsetY-10+"px",tooltip.setText(`${c.label}${c.weight!==void 0?` (\xD7${c.weight})`:""}`)}),v.on("mouseout",()=>{tooltip.style.opacity="0"}),this.simulation=o.forceSimulation(g).force("link",o.forceLink(h).id(t=>t.id).distance(80)).force("charge",o.forceManyBody().strength(-200)).force("center",o.forceCenter(d/2,p/2)).force("collision",o.forceCollide(20)).on("tick",()=>{q.attr("x1",t=>t.source.x).attr("y1",t=>t.source.y).attr("x2",t=>t.target.x).attr("y2",t=>t.target.y),v.attr("transform",t=>`translate(${t.x},${t.y})`)})}truncate(e,i){return e?e.length>i?e.slice(0,i)+"\u2026":e:""}};var A=require("obsidian");var T=class extends A.Modal{constructor(e){super(e);s(this,"results",[]);s(this,"inputEl");s(this,"listEl");s(this,"loading",!1);s(this,"debounceTimer",null);s(this,"onSelect")}onOpen(){this.contentEl.createEl("style",{text:`
|
||||
.zhiyi-search-modal { width: 600px; max-height: 80vh; display: flex; flex-direction: column; }
|
||||
.zhiyi-search-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 12px 16px; border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.zhiyi-search-header h2 { margin: 0; font-size: 14px; font-weight: 600; flex: 1; }
|
||||
.zhiyi-search-input {
|
||||
flex: 1; padding: 8px 12px; font-size: 14px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px; color: var(--text-normal); outline: none;
|
||||
}
|
||||
.zhiyi-search-input:focus { border-color: var(--text-accent); }
|
||||
.zhiyi-search-results { flex: 1; overflow-y: auto; padding: 8px 0; }
|
||||
.zhiyi-search-result {
|
||||
padding: 10px 16px; border-bottom: 1px solid var(--background-modifier-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.zhiyi-search-result:hover { background: var(--background-modifier-hover); }
|
||||
.zhiyi-search-result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||
.zhiyi-search-score { font-size: 10px; color: var(--text-muted); }
|
||||
.zhiyi-search-cat { font-size: 10px; background: var(--background-secondary); padding: 1px 4px; border-radius: 2px; color: var(--text-muted); }
|
||||
.zhiyi-search-content { font-size: 12px; color: var(--text-normal); line-height: 1.5; }
|
||||
.zhiyi-search-id { font-size: 10px; color: var(--text-faint); font-family: monospace; margin-top: 3px; }
|
||||
.zhiyi-search-loading { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
.zhiyi-search-empty { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
.zhiyi-search-hint { padding: 8px 16px; font-size: 11px; color: var(--text-muted); text-align: center; }
|
||||
`});let e=this.contentEl.createEl("div",{cls:"zhiyi-search-modal"}),i=e.createEl("div",{cls:"zhiyi-search-header"});i.createEl("h2",{text:"\u{1F50D} \u7EC7\u5FC6\u8BED\u4E49\u641C\u7D22"}),this.inputEl=i.createEl("input",{cls:"zhiyi-search-input",attr:{placeholder:"\u8F93\u5165\u5173\u952E\u8BCD\uFF0C\u8BED\u4E49\u641C\u7D22\u8BB0\u5FC6\u2026",autofocus:"true"}}),this.listEl=e.createEl("div",{cls:"zhiyi-search-results"}),e.createEl("div",{cls:"zhiyi-search-hint",text:"\u5B9E\u65F6\u641C\u7D22 \xB7 \u6309 Enter \u9009\u62E9\u7B2C\u4E00\u6761\u7ED3\u679C"}),this.inputEl.oninput=()=>{this.debounceTimer!==null&&window.clearTimeout(this.debounceTimer),this.debounceTimer=window.setTimeout(()=>this.doSearch(),300)},this.inputEl.onkeydown=n=>{n.key==="Enter"&&this.results.length>0&&this.selectResult(this.results[0]),n.key==="Escape"&&this.close()}}async doSearch(){let e=this.inputEl.value.trim();if(!e){this.results=[],this.listEl.empty();return}this.listEl.innerHTML='<div class="zhiyi-search-loading">\u641C\u7D22\u4E2D\u2026</div>',this.loading=!0;try{let i=await R(e,"hermes-main");this.results=i.memories||[],this.renderResults()}catch(i){this.listEl.innerHTML=`<div class="zhiyi-search-empty">\u641C\u7D22\u5931\u8D25: ${i.message}</div>`}}renderResults(){if(this.listEl.empty(),this.results.length===0){this.listEl.createEl("div",{cls:"zhiyi-search-empty",text:"\u672A\u627E\u5230\u7ED3\u679C"});return}for(let e of this.results){let i=this.listEl.createEl("div",{cls:"zhiyi-search-result"}),n=i.createEl("div",{cls:"zhiyi-search-result-header"});n.createEl("span",{cls:"zhiyi-search-cat",text:e.category||"normal"}),n.createEl("span",{cls:"zhiyi-search-score",text:` relevance: ${(e.score??0).toFixed(3)}`}),i.createEl("div",{cls:"zhiyi-search-content",text:this.truncate(e.content,200)}),i.createEl("div",{cls:"zhiyi-search-id",text:e.id}),i.onclick=()=>this.selectResult(e)}}selectResult(e){this.onSelect?.(e.id,e.content),this.close()}truncate(e,i){return e?e.length>i?e.slice(0,i)+"\u2026":e:""}onClose(){this.listEl.empty()}};var M=class extends m.Plugin{constructor(){super(...arguments);s(this,"ribbonIcon",null)}async onload(){this.registerView(b,e=>new k(e)),this.registerView(w,e=>new S(e)),this.addCommand({id:"zhiyi-open-memory-view",name:"\u6253\u5F00\u7EC7\u5FC6\u8BB0\u5FC6\u9762\u677F",callback:()=>this.openMemoryView()}),this.addCommand({id:"zhiyi-open-graph-view",name:"\u6253\u5F00\u7EC7\u5FC6\u56FE\u8C31\u9762\u677F",callback:()=>this.openGraphView()}),this.addCommand({id:"zhiyi-search-memories",name:"\u641C\u7D22\u7EC7\u5FC6\u8BB0\u5FC6",callback:()=>this.openSearchModal()}),this.addStatusBarItem().setText("\u{1F9E0} \u7EC7\u5FC6"),this.addStatusBarItem().setText("Ctrl+P \u2192 \u7EC7\u5FC6"),this.addSettingTab(new G(this.app,this))}onunload(){this.app.workspace.getLeavesOfType(b).forEach(e=>e.detach()),this.app.workspace.getLeavesOfType(w).forEach(e=>e.detach())}async openMemoryView(){let e=this.app.workspace.getLeaf("right");await e.setViewState({type:b,active:!0}),this.app.workspace.revealLeaf(e)}async openGraphView(){let e=this.app.workspace.getLeaf("right");await e.setViewState({type:w,active:!0}),this.app.workspace.revealLeaf(e)}openSearchModal(){let e=new T(this.app);e.onSelect=(i,n)=>{navigator.clipboard.writeText(n).catch(()=>{}),new this.app.notify(`\u5DF2\u590D\u5236: ${n.slice(0,80)}\u2026`)},e.open()}},G=class extends m.PluginSettingTab{constructor(r,e){super(r,e)}display(){let{containerEl:r}=this;r.empty(),r.createEl("h2",{text:"\u{1F9E0} \u7EC7\u5FC6\u8BBE\u7F6E"}),new m.Setting(r).setName("API \u5730\u5740").setDesc("\u7EC7\u5FC6 Go API \u670D\u52A1\u5730\u5740\uFF08\u9ED8\u8BA4 http://localhost:7821\uFF09").addText(e=>e.setPlaceholder("http://localhost:7821").setValue("http://localhost:7821").onChange(i=>{window.zhiyi_api_url=i})),new m.Setting(r).setName("API Key").setDesc("\u7EC7\u5FC6\u8BA4\u8BC1\u5BC6\u94A5").addText(e=>e.setPlaceholder("zhiyi-dev-key-2026").onChange(i=>{window.zhiyi_api_key=i})),new m.Setting(r).setName("\u9ED8\u8BA4 Namespace").setDesc("\u641C\u7D22\u548C\u67E5\u8BE2\u4F7F\u7528\u7684\u9ED8\u8BA4\u547D\u540D\u7A7A\u95F4").addText(e=>e.setValue("hermes-main").onChange(i=>{window.zhiyi_namespace=i})),new m.Setting(r).setName("\u6253\u5F00\u8BB0\u5FC6\u9762\u677F").addButton(e=>e.setButtonText("\u6253\u5F00 \u{1F9E0}").onClick(()=>{this.plugin.openMemoryView()})),new m.Setting(r).setName("\u6253\u5F00\u56FE\u8C31\u9762\u677F").addButton(e=>e.setButtonText("\u6253\u5F00 \u{1F4D0}").onClick(()=>{this.plugin.openGraphView()}))}};
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { App, Plugin, PluginSettingTab, Setting, addIcon } from "obsidian";
|
||||
import { MemoryView, MEMORY_VIEW_TYPE } from "./src/MemoryView";
|
||||
import { GraphView, GRAPH_VIEW_TYPE } from "./src/GraphView";
|
||||
import { SearchModal } from "./src/SearchModal";
|
||||
|
||||
export default class ZhiYiPlugin extends Plugin {
|
||||
private ribbonIcon: HTMLElement | null = null;
|
||||
|
||||
async onload() {
|
||||
// ─── 注册视图 ─────────────────────────────────────────────────────
|
||||
this.registerView(MEMORY_VIEW_TYPE, (leaf) => new MemoryView(leaf));
|
||||
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf));
|
||||
|
||||
// ─── 侧边栏命令 ───────────────────────────────────────────────────
|
||||
// 记忆面板
|
||||
this.addCommand({
|
||||
id: "zhiyi-open-memory-view",
|
||||
name: "打开织忆记忆面板",
|
||||
callback: () => this.openMemoryView(),
|
||||
});
|
||||
|
||||
// 图谱面板
|
||||
this.addCommand({
|
||||
id: "zhiyi-open-graph-view",
|
||||
name: "打开织忆图谱面板",
|
||||
callback: () => this.openGraphView(),
|
||||
});
|
||||
|
||||
// 搜索模态框
|
||||
this.addCommand({
|
||||
id: "zhiyi-search-memories",
|
||||
name: "搜索织忆记忆",
|
||||
callback: () => this.openSearchModal(),
|
||||
});
|
||||
|
||||
// ─── 状态栏 ────────────────────────────────────────────────────────
|
||||
this.addStatusBarItem().setText("🧠 织忆");
|
||||
this.addStatusBarItem().setText("Ctrl+P → 织忆");
|
||||
|
||||
// ─── 设置页 ────────────────────────────────────────────────────────
|
||||
this.addSettingTab(new ZhiYiSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// 关闭所有视图
|
||||
this.app.workspace.getLeavesOfType(MEMORY_VIEW_TYPE).forEach((leaf) => leaf.detach());
|
||||
this.app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE).forEach((leaf) => leaf.detach());
|
||||
}
|
||||
|
||||
async openMemoryView() {
|
||||
const leaf = this.app.workspace.getLeaf("right");
|
||||
await leaf.setViewState({ type: MEMORY_VIEW_TYPE, active: true });
|
||||
this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
async openGraphView() {
|
||||
const leaf = this.app.workspace.getLeaf("right");
|
||||
await leaf.setViewState({ type: GRAPH_VIEW_TYPE, active: true });
|
||||
this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
openSearchModal() {
|
||||
const modal = new SearchModal(this.app);
|
||||
modal.onSelect = (id, content) => {
|
||||
// 选好后复制到剪贴板并在通知中显示
|
||||
navigator.clipboard.writeText(content).catch(() => {});
|
||||
new (this.app as unknown as { notify: (m: string) => void }).notify(
|
||||
`已复制: ${content.slice(0, 80)}…`
|
||||
);
|
||||
};
|
||||
modal.open();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 设置页 ────────────────────────────────────────────────────────────────
|
||||
|
||||
class ZhiYiSettingTab extends PluginSettingTab {
|
||||
constructor(app: App, plugin: ZhiYiPlugin) {
|
||||
super(app, plugin);
|
||||
}
|
||||
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "🧠 织忆设置" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("API 地址")
|
||||
.setDesc("织忆 Go API 服务地址(默认 http://localhost:7821)")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("http://localhost:7821")
|
||||
.setValue("http://localhost:7821")
|
||||
.onChange((val) => {
|
||||
// API 地址暂存(后续在 api.ts 中使用)
|
||||
(window as unknown as Record<string, string>)["zhiyi_api_url"] = val;
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("API Key")
|
||||
.setDesc("织忆认证密钥")
|
||||
.addText((text) =>
|
||||
text.setPlaceholder("zhiyi-dev-key-2026").onChange((val) => {
|
||||
(window as unknown as Record<string, string>)["zhiyi_api_key"] = val;
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("默认 Namespace")
|
||||
.setDesc("搜索和查询使用的默认命名空间")
|
||||
.addText((text) =>
|
||||
text.setValue("hermes-main").onChange((val) => {
|
||||
(window as unknown as Record<string, string>)["zhiyi_namespace"] = val;
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("打开记忆面板")
|
||||
.addButton((btn) =>
|
||||
btn.setButtonText("打开 🧠").onClick(() => {
|
||||
(this.plugin as unknown as { openMemoryView: () => void }).openMemoryView();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("打开图谱面板")
|
||||
.addButton((btn) =>
|
||||
btn.setButtonText("打开 📐").onClick(() => {
|
||||
(this.plugin as unknown as { openGraphView: () => void }).openGraphView();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "zhiyi-memory",
|
||||
"name": "织忆",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "织忆记忆系统可视化 — 图谱探索、记忆搜索、蒸馏状态监控",
|
||||
"author": "小唯 A06",
|
||||
"fundingurl": "",
|
||||
"isDesktopOnly": false,
|
||||
"jsEngine": "esbuild",
|
||||
"main": "main.js",
|
||||
"styles": ["styles.css"]
|
||||
}
|
||||
|
|
@ -0,0 +1,473 @@
|
|||
{
|
||||
"name": "obsidian",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "obsidian",
|
||||
"version": "1.0.0",
|
||||
"description": "> 给织忆记忆系统提供 Obsidian 内置可视化界面。",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.28.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||
import { fetchGraphExport, fetchNeighbors, fetchByEntity, fetchPageRank, type GraphNode, type GraphEdge, type MemoryRecord } from "./api";
|
||||
|
||||
// 动态加载 D3.js(从 CDN,不打包)
|
||||
async function loadD3(): Promise<typeof import("d3")> {
|
||||
if ((window as unknown as Record<string, unknown>)["d3"]) {
|
||||
return (window as unknown as Record<string, unknown>)["d3"] as typeof import("d3");
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js";
|
||||
script.onload = () => resolve((window as unknown as Record<string, unknown>)["d3"] as typeof import("d3"));
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
export const GRAPH_VIEW_TYPE = "zhiyi-graph-view";
|
||||
|
||||
interface SimNode extends d3.SimulationNodeDatum {
|
||||
id: string;
|
||||
label: string;
|
||||
category?: string;
|
||||
weight?: number;
|
||||
isCentral?: boolean;
|
||||
}
|
||||
|
||||
interface SimLink extends d3.SimulationLinkDatum<SimNode> {
|
||||
relation: string;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export class GraphView extends ItemView {
|
||||
private container: HTMLElement;
|
||||
private currentEntity = "";
|
||||
private d3: typeof import("d3") | null = null;
|
||||
private svg: d3.Selection<SVGSVGElement, unknown, null, undefined> | null = null;
|
||||
private simulation: d3.Simulation<SimNode, SimLink> | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
this.container = this.contentEl;
|
||||
}
|
||||
|
||||
getViewType() { return GRAPH_VIEW_TYPE; }
|
||||
getDisplayText() { return "织忆图谱"; }
|
||||
|
||||
async onOpen() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.simulation?.stop();
|
||||
}
|
||||
|
||||
async render() {
|
||||
this.container.empty();
|
||||
this.container.createEl("style", {
|
||||
text: `
|
||||
.zhiyi-graph-view { height: 100%; display: flex; flex-direction: column; }
|
||||
.zhiyi-graph-toolbar {
|
||||
display: flex; gap: 6px; padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
align-items: center;
|
||||
}
|
||||
.zhiyi-graph-toolbar input {
|
||||
flex: 1; padding: 4px 8px; font-size: 12px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px; color: var(--text-normal);
|
||||
}
|
||||
.zhiyi-graph-toolbar button {
|
||||
padding: 4px 10px; font-size: 12px; cursor: pointer;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.zhiyi-graph-toolbar .info {
|
||||
font-size: 11px; color: var(--text-muted); padding: 0 6px;
|
||||
}
|
||||
.zhiyi-graph-canvas { flex: 1; position: relative; overflow: hidden; }
|
||||
.zhiyi-graph-canvas svg { width: 100%; height: 100%; }
|
||||
.zhiyi-graph-detail {
|
||||
position: absolute; bottom: 0; left: 0; right: 0;
|
||||
background: var(--background-secondary);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding: 8px 12px; font-size: 11px; max-height: 120px; overflow-y: auto;
|
||||
}
|
||||
.zhiyi-graph-detail-title { font-weight: 600; margin-bottom: 4px; }
|
||||
.zhiyi-graph-node-label { font-size: 10px; fill: var(--text-muted); pointer-events: none; }
|
||||
.zhiyi-legend { position: absolute; top: 8px; right: 8px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 10px; }
|
||||
.zhiyi-legend-item { display: flex; align-items: center; gap: 4px; margin: 2px 0; }
|
||||
.zhiyi-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.zhiyi-tooltip {
|
||||
position: absolute; background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px; padding: 6px 8px; font-size: 11px;
|
||||
pointer-events: none; opacity: 0; transition: opacity 0.15s;
|
||||
max-width: 200px; z-index: 10;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const root = this.container.createEl("div", { cls: "zhiyi-graph-view" });
|
||||
const toolbar = root.createEl("div", { cls: "zhiyi-graph-toolbar" });
|
||||
|
||||
const searchInput = toolbar.createEl("input", {
|
||||
attr: { placeholder: "输入实体名称,回车查看 ego-network(如: 牧尘)" }
|
||||
}) as HTMLInputElement;
|
||||
|
||||
const loadBtn = toolbar.createEl("button", { text: "加载图谱" });
|
||||
toolbar.createEl("span", { cls: "info", text: "点击节点查看详情" });
|
||||
|
||||
// 快捷入口:Top 实体
|
||||
const topBtn = toolbar.createEl("button", { text: "Top 节点" });
|
||||
|
||||
const canvas = root.createEl("div", { cls: "zhiyi-graph-canvas" });
|
||||
const tooltip = canvas.createEl("div", { cls: "zhiyi-tooltip" });
|
||||
|
||||
const legendEl = canvas.createEl("div", { cls: "zhiyi-legend" });
|
||||
legendEl.innerHTML = `
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#4a9eff"></span> 默认</div>
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#f6c945"></span> 人物</div>
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#34c759"></span> 项目</div>
|
||||
<div class="zhiyi-legend-item"><span class="zhiyi-legend-dot" style="background:#ff6b6b"></span> 事件</div>
|
||||
`;
|
||||
|
||||
const detail = root.createEl("div", { cls: "zhiyi-graph-detail", attr: { style: "display:none" } });
|
||||
const detailTitle = detail.createEl("div", { cls: "zhiyi-graph-detail-title" });
|
||||
const detailContent = detail.createEl("div", { cls: "zhiyi-graph-detail-content" });
|
||||
|
||||
// 加载 D3 并渲染全局图谱
|
||||
this.d3 = await loadD3();
|
||||
this.svg = this.d3.select(canvas.createEl("svg"));
|
||||
await this.renderGlobalGraph();
|
||||
|
||||
loadBtn.onclick = async () => {
|
||||
if (searchInput.value.trim()) {
|
||||
await this.renderEgoGraph(searchInput.value.trim(), canvas, tooltip, detail, detailTitle, detailContent);
|
||||
}
|
||||
};
|
||||
|
||||
searchInput.onkeydown = async (e) => {
|
||||
if (e.key === "Enter" && searchInput.value.trim()) {
|
||||
await this.renderEgoGraph(searchInput.value.trim(), canvas, tooltip, detail, detailTitle, detailContent);
|
||||
}
|
||||
};
|
||||
|
||||
topBtn.onclick = async () => {
|
||||
try {
|
||||
const data = await fetchPageRank(20);
|
||||
const topEntity = data.pagerank?.[0]?.entity || "牧尘";
|
||||
searchInput.value = topEntity;
|
||||
await this.renderEgoGraph(topEntity, canvas, tooltip, detail, detailTitle, detailContent);
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
private async renderGlobalGraph() {
|
||||
try {
|
||||
const data = await fetchGraphExport("hermes-main", 80);
|
||||
await this.renderD3Graph(data.nodes, data.edges, null);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
private async renderEgoGraph(
|
||||
entity: string,
|
||||
canvas: HTMLElement,
|
||||
tooltip: HTMLElement,
|
||||
detail: HTMLElement,
|
||||
detailTitle: HTMLElement,
|
||||
detailContent: HTMLElement
|
||||
) {
|
||||
this.currentEntity = entity;
|
||||
this.simulation?.stop();
|
||||
try {
|
||||
const [graphData, memData] = await Promise.all([
|
||||
fetchNeighbors(entity),
|
||||
fetchByEntity(entity),
|
||||
]);
|
||||
// 中心节点
|
||||
const central: GraphNode = { id: entity, label: entity };
|
||||
const nodes = [central, ...graphData.nodes.filter(n => n.id !== entity)];
|
||||
await this.renderD3Graph(nodes, graphData.edges, entity);
|
||||
|
||||
// 详情面板
|
||||
detail.style.display = "block";
|
||||
detailTitle.setText(`📌 ${entity}`);
|
||||
const mems = memData.memories || [];
|
||||
detailContent.innerHTML = mems.slice(0, 3).map((m: MemoryRecord) =>
|
||||
`<div style="margin:2px 0">• ${this.truncate(m.content, 100)}</div>`
|
||||
).join("") || "<div>无关联记忆</div>";
|
||||
} catch (e) {
|
||||
const errEl = canvas.createEl("div", { text: `加载失败: ${(e as Error).message}`, cls: "zhiyi-empty" });
|
||||
setTimeout(() => errEl.remove(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
private async renderD3Graph(
|
||||
nodes: GraphNode[],
|
||||
edges: GraphEdge[],
|
||||
centralId: string | null
|
||||
) {
|
||||
if (!this.d3 || !this.svg) return;
|
||||
const d3 = this.d3;
|
||||
const svg = this.svg;
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
const width = svg.node()!.clientWidth || 600;
|
||||
const height = svg.node()!.clientHeight || 400;
|
||||
|
||||
const catColor: Record<string, string> = {
|
||||
"人物": "#f6c945", "人物_name": "#f6c945", "project": "#34c759",
|
||||
"项目": "#34c759", "event": "#ff6b6b", "事件": "#ff6b6b",
|
||||
"concept": "#a855f7", "概念": "#a855f7", "location": "#06b6d4",
|
||||
"distilled": "#4a9eff", "default": "#4a9eff"
|
||||
};
|
||||
|
||||
const getColor = (n: GraphNode) => {
|
||||
if (n.id === centralId) return "#ff4d4d";
|
||||
if (n.category && catColor[n.category]) return catColor[n.category];
|
||||
return catColor[n.weight !== undefined && n.weight > 5 ? "distilled" : "default"];
|
||||
};
|
||||
|
||||
const simNodes: SimNode[] = nodes.map(n => ({
|
||||
id: n.id, label: n.label || n.id,
|
||||
category: n.category, weight: n.weight,
|
||||
isCentral: n.id === centralId,
|
||||
}));
|
||||
|
||||
const nodeMap = new Map(simNodes.map(n => [n.id, n]));
|
||||
const simLinks: SimLink[] = edges
|
||||
.filter(e => nodeMap.has(e.source) && nodeMap.has(e.target))
|
||||
.map(e => ({ source: e.source, target: e.target, relation: e.relation, weight: e.weight }));
|
||||
|
||||
// 缩放
|
||||
const zoom = d3.zoom<SVGSVGElement, unknown>()
|
||||
.scaleExtent([0.2, 5])
|
||||
.on("zoom", (event) => {
|
||||
g.attr("transform", event.transform);
|
||||
});
|
||||
svg.call(zoom as unknown as (s: d3.Selection<SVGSVGElement, unknown, null, undefined>) => void);
|
||||
|
||||
const g = svg.append("g");
|
||||
svg.append("defs").append("marker")
|
||||
.attr("id", "arrowhead")
|
||||
.attr("viewBox", "-0 -5 10 10")
|
||||
.attr("refX", 15).attr("refY", 0)
|
||||
.attr("orient", "auto")
|
||||
.attr("markerWidth", 6).attr("markerHeight", 6)
|
||||
.append("path").attr("d", "M 0,-5 L 10,0 L 0,5")
|
||||
.attr("fill", "#666");
|
||||
|
||||
const link = g.append("g").selectAll("line")
|
||||
.data(simLinks)
|
||||
.enter().append("line")
|
||||
.attr("stroke", "#999").attr("stroke-opacity", 0.6)
|
||||
.attr("stroke-width", d => Math.min(2, d.weight || 1))
|
||||
.attr("marker-end", "url(#arrowhead)");
|
||||
|
||||
const node = g.append("g").selectAll("g")
|
||||
.data(simNodes)
|
||||
.enter().append("g")
|
||||
.style("cursor", "pointer");
|
||||
|
||||
node.append("circle")
|
||||
.attr("r", d => d.isCentral ? 12 : 6 + (d.weight || 1) * 0.5)
|
||||
.attr("fill", d => getColor(d as GraphNode))
|
||||
.attr("stroke", d => d.isCentral ? "#ff4d4d" : "white")
|
||||
.attr("stroke-width", d => d.isCentral ? 2 : 1.5);
|
||||
|
||||
node.append("text")
|
||||
.text(d => d.label.length > 12 ? d.label.slice(0, 12) + "…" : d.label)
|
||||
.attr("dy", -8)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("class", "zhiyi-graph-node-label")
|
||||
.style("font-size", d => d.isCentral ? "11px" : "9px")
|
||||
.style("fill", "var(--text-muted)");
|
||||
|
||||
// 拖拽
|
||||
const drag = d3.drag<SVGGElement, SimNode>()
|
||||
.on("start", (event, d) => {
|
||||
if (!event.active) this.simulation!.alphaTarget(0.3).restart();
|
||||
d.fx = d.x; d.fy = d.y;
|
||||
})
|
||||
.on("drag", (event, d) => { d.fx = event.x; d.fy = event.y; })
|
||||
.on("end", (event, d) => {
|
||||
if (!event.active) this.simulation!.alphaTarget(0);
|
||||
d.fx = null; d.fy = null;
|
||||
});
|
||||
node.call(drag as unknown as (s: d3.Selection<SVGGElement, SimNode, SVGGElement, unknown>) => void);
|
||||
|
||||
node.on("click", (_event, d) => {
|
||||
this.renderEgoGraph(d.id, canvas.parentElement!, tooltip,
|
||||
canvas.parentElement!.querySelector(".zhiyi-graph-detail") as HTMLElement,
|
||||
canvas.parentElement!.querySelector(".zhiyi-graph-detail-title") as HTMLElement,
|
||||
canvas.parentElement!.querySelector(".zhiyi-graph-detail-content") as HTMLElement);
|
||||
});
|
||||
|
||||
node.on("mouseover", (event, d) => {
|
||||
tooltip.style.opacity = "1";
|
||||
tooltip.style.left = (event.offsetX + 10) + "px";
|
||||
tooltip.style.top = (event.offsetY - 10) + "px";
|
||||
tooltip.setText(`${d.label}${d.weight !== undefined ? ` (×${d.weight})` : ""}`);
|
||||
});
|
||||
|
||||
node.on("mouseout", () => { tooltip.style.opacity = "0"; });
|
||||
|
||||
this.simulation = d3.forceSimulation(simNodes)
|
||||
.force("link", d3.forceLink<SimNode, SimLink>(simLinks).id(d => d.id).distance(80))
|
||||
.force("charge", d3.forceManyBody().strength(-200))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force("collision", d3.forceCollide(20))
|
||||
.on("tick", () => {
|
||||
link
|
||||
.attr("x1", d => (d.source as SimNode).x!)
|
||||
.attr("y1", d => (d.source as SimNode).y!)
|
||||
.attr("x2", d => (d.target as SimNode).x!)
|
||||
.attr("y2", d => (d.target as SimNode).y!);
|
||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
}
|
||||
|
||||
private truncate(s: string, max: number) {
|
||||
if (!s) return "";
|
||||
return s.length > max ? s.slice(0, max) + "…" : s;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||
import { fetchMemories, fetchDistillStatus, fetchDistillQuota, type MemoryRecord } from "./api";
|
||||
|
||||
export const MEMORY_VIEW_TYPE = "zhiyi-memory-view";
|
||||
|
||||
export class MemoryView extends ItemView {
|
||||
private container: HTMLElement;
|
||||
private memories: MemoryRecord[] = [];
|
||||
private currentPage = 0;
|
||||
private pageSize = 20;
|
||||
private totalMemories = 0;
|
||||
private status_interval: number | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
this.container = this.contentEl;
|
||||
}
|
||||
|
||||
getViewType() { return MEMORY_VIEW_TYPE; }
|
||||
getDisplayText() { return "织忆记忆"; }
|
||||
|
||||
async onOpen() {
|
||||
this.render();
|
||||
this.startStatusPoll();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
if (this.status_interval !== null) {
|
||||
(window as unknown as { clearInterval: (n: number) => void }).clearInterval(this.status_interval);
|
||||
}
|
||||
}
|
||||
|
||||
private startStatusPoll() {
|
||||
// 轮询蒸馏状态(每 30 秒)
|
||||
const id = (window as unknown as { setInterval: (fn: () => void, ms: number) => number }).setInterval(
|
||||
() => this.renderDistillBanner(),
|
||||
30000
|
||||
);
|
||||
this.status_interval = id;
|
||||
}
|
||||
|
||||
private async renderDistillBanner() {
|
||||
const banner = this.container.querySelector(".zhiyi-distill-banner") as HTMLElement;
|
||||
if (!banner) return;
|
||||
try {
|
||||
const [status, quota] = await Promise.all([fetchDistillStatus(), fetchDistillQuota()]);
|
||||
if (status.queue_len > 0) {
|
||||
banner.innerHTML = `<span class="zhiyi-warn">⚠️ 蒸馏队列: ${status.queue_len} 条</span>`;
|
||||
banner.style.display = "block";
|
||||
} else {
|
||||
banner.style.display = "none";
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async render() {
|
||||
this.container.empty();
|
||||
this.container.createEl("style", {
|
||||
text: `
|
||||
.zhiyi-memory-view { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
|
||||
.zhiyi-header { padding: 10px 12px 6px; border-bottom: 1px solid var(--background-modifier-border); }
|
||||
.zhiyi-header h1 { font-size: 14px; font-weight: 600; margin: 0 0 4px; }
|
||||
.zhiyi-stats { color: var(--text-muted); font-size: 11px; margin: 0; }
|
||||
.zhiyi-distill-banner {
|
||||
display: none; background: #c44; color: white; padding: 4px 12px;
|
||||
font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
.zhiyi-body { flex: 1; overflow-y: auto; }
|
||||
.zhiyi-item {
|
||||
padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.zhiyi-item:hover { background: var(--background-modifier-hover); }
|
||||
.zhiyi-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; }
|
||||
.zhiyi-item-cat { font-size: 10px; color: var(--text-muted); background: var(--background-secondary); padding: 1px 4px; border-radius: 2px; }
|
||||
.zhiyi-item-score { font-size: 10px; color: var(--text-muted); }
|
||||
.zhiyi-item-content { font-size: 12px; color: var(--text-normal); line-height: 1.4; }
|
||||
.zhiyi-item-time { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
||||
.zhiyi-item-id { font-size: 10px; color: var(--text-faint); font-family: monospace; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.zhiyi-pagination { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--background-modifier-border); }
|
||||
.zhiyi-pagination button { flex: 1; padding: 4px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; font-size: 11px; }
|
||||
.zhiyi-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.zhiyi-loading { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
.zhiyi-empty { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
`
|
||||
});
|
||||
this.container.createEl("div", { cls: "zhiyi-memory-view" }).append(
|
||||
this.container.createEl("div", { cls: "zhiyi-distill-banner" }),
|
||||
this.container.createEl("div", { cls: "zhiyi-header" }).append(
|
||||
this.container.createEl("h1", { text: "🧠 织忆记忆" }),
|
||||
this.container.createEl("p", { cls: "zhiyi-stats", text: "加载中..." })
|
||||
),
|
||||
this.container.createEl("div", { cls: "zhiyi-body" }),
|
||||
this.container.createEl("div", { cls: "zhiyi-pagination" })
|
||||
);
|
||||
await this.loadMemories();
|
||||
await this.renderDistillBanner();
|
||||
}
|
||||
|
||||
async loadMemories() {
|
||||
const body = this.container.querySelector(".zhiyi-body") as HTMLElement;
|
||||
const stats = this.container.querySelector(".zhiyi-stats") as HTMLElement;
|
||||
body.innerHTML = '<div class="zhiyi-loading">加载中…</div>';
|
||||
try {
|
||||
const data = await fetchMemories({ namespace: "hermes-main", limit: this.pageSize, offset: this.currentPage * this.pageSize });
|
||||
this.memories = data.memories || [];
|
||||
this.totalMemories = data.total || 0;
|
||||
stats.setText(`${this.totalMemories} 条记忆 · 第 ${this.currentPage + 1} 页`);
|
||||
this.renderList();
|
||||
} catch (e) {
|
||||
body.innerHTML = `<div class="zhiyi-empty">加载失败: ${(e as Error).message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
private renderList() {
|
||||
const body = this.container.querySelector(".zhiyi-body") as HTMLElement;
|
||||
const pag = this.container.querySelector(".zhiyi-pagination") as HTMLElement;
|
||||
body.empty();
|
||||
if (this.memories.length === 0) {
|
||||
body.createEl("div", { cls: "zhiyi-empty", text: "暂无记忆" });
|
||||
}
|
||||
for (const mem of this.memories) {
|
||||
const item = body.createEl("div", { cls: "zhiyi-item" });
|
||||
const header = item.createEl("div", { cls: "zhiyi-item-header" });
|
||||
header.createEl("span", { cls: "zhiyi-item-cat", text: mem.category || "normal" });
|
||||
header.createEl("span", { cls: "zhiyi-item-score", text: `score: ${(mem.quality_score ?? 0).toFixed(2)}` });
|
||||
item.createEl("div", { cls: "zhiyi-item-content", text: this.truncate(mem.content, 120) });
|
||||
item.createEl("div", { cls: "zhiyi-item-time", text: this.formatTime(mem.created_at) });
|
||||
item.createEl("div", { cls: "zhiyi-item-id", text: mem.id });
|
||||
}
|
||||
const totalPages = Math.ceil(this.totalMemories / this.pageSize);
|
||||
pag.empty();
|
||||
if (totalPages > 1) {
|
||||
const prev = pag.createEl("button", { text: "◀ 上一页" });
|
||||
prev.setAttr("disabled", this.currentPage === 0 ? "true" : "");
|
||||
prev.onclick = () => { this.currentPage--; this.loadMemories(); };
|
||||
pag.createEl("span", { text: `${this.currentPage + 1}/${totalPages}`, cls: "zhiyi-item-cat" });
|
||||
const next = pag.createEl("button", { text: "下一页 ▶" });
|
||||
next.setAttr("disabled", this.currentPage >= totalPages - 1 ? "true" : "");
|
||||
next.onclick = () => { this.currentPage++; this.loadMemories(); };
|
||||
}
|
||||
}
|
||||
|
||||
private truncate(s: string, max: number) {
|
||||
if (!s) return "";
|
||||
return s.length > max ? s.slice(0, max) + "…" : s;
|
||||
}
|
||||
|
||||
private formatTime(iso: string) {
|
||||
if (!iso) return "";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||||
} catch { return iso; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import { App, Modal, setIcon } from "obsidian";
|
||||
import { searchMemories, type RecallResult } from "./api";
|
||||
|
||||
export class SearchModal extends Modal {
|
||||
private results: RecallResult["memories"] = [];
|
||||
private inputEl: HTMLInputElement;
|
||||
private listEl: HTMLElement;
|
||||
private loading = false;
|
||||
private debounceTimer: number | null = null;
|
||||
onSelect?: (id: string, content: string) => void;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
this.contentEl.createEl("style", {
|
||||
text: `
|
||||
.zhiyi-search-modal { width: 600px; max-height: 80vh; display: flex; flex-direction: column; }
|
||||
.zhiyi-search-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 12px 16px; border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.zhiyi-search-header h2 { margin: 0; font-size: 14px; font-weight: 600; flex: 1; }
|
||||
.zhiyi-search-input {
|
||||
flex: 1; padding: 8px 12px; font-size: 14px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px; color: var(--text-normal); outline: none;
|
||||
}
|
||||
.zhiyi-search-input:focus { border-color: var(--text-accent); }
|
||||
.zhiyi-search-results { flex: 1; overflow-y: auto; padding: 8px 0; }
|
||||
.zhiyi-search-result {
|
||||
padding: 10px 16px; border-bottom: 1px solid var(--background-modifier-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.zhiyi-search-result:hover { background: var(--background-modifier-hover); }
|
||||
.zhiyi-search-result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||
.zhiyi-search-score { font-size: 10px; color: var(--text-muted); }
|
||||
.zhiyi-search-cat { font-size: 10px; background: var(--background-secondary); padding: 1px 4px; border-radius: 2px; color: var(--text-muted); }
|
||||
.zhiyi-search-content { font-size: 12px; color: var(--text-normal); line-height: 1.5; }
|
||||
.zhiyi-search-id { font-size: 10px; color: var(--text-faint); font-family: monospace; margin-top: 3px; }
|
||||
.zhiyi-search-loading { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
.zhiyi-search-empty { padding: 20px; text-align: center; color: var(--text-muted); }
|
||||
.zhiyi-search-hint { padding: 8px 16px; font-size: 11px; color: var(--text-muted); text-align: center; }
|
||||
`
|
||||
});
|
||||
|
||||
const root = this.contentEl.createEl("div", { cls: "zhiyi-search-modal" });
|
||||
const header = root.createEl("div", { cls: "zhiyi-search-header" });
|
||||
header.createEl("h2", { text: "🔍 织忆语义搜索" });
|
||||
this.inputEl = header.createEl("input", {
|
||||
cls: "zhiyi-search-input",
|
||||
attr: { placeholder: "输入关键词,语义搜索记忆…", autofocus: "true" }
|
||||
}) as HTMLInputElement;
|
||||
this.listEl = root.createEl("div", { cls: "zhiyi-search-results" });
|
||||
root.createEl("div", { cls: "zhiyi-search-hint", text: "实时搜索 · 按 Enter 选择第一条结果" });
|
||||
|
||||
this.inputEl.oninput = () => {
|
||||
if (this.debounceTimer !== null) {
|
||||
(window as unknown as { clearTimeout: (n: number) => void }).clearTimeout(this.debounceTimer);
|
||||
}
|
||||
this.debounceTimer = (window as unknown as { setTimeout: (fn: () => void, ms: number) => number }).setTimeout(
|
||||
() => this.doSearch(),
|
||||
300
|
||||
);
|
||||
};
|
||||
|
||||
this.inputEl.onkeydown = (e) => {
|
||||
if (e.key === "Enter" && this.results.length > 0) {
|
||||
this.selectResult(this.results[0]);
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async doSearch() {
|
||||
const query = this.inputEl.value.trim();
|
||||
if (!query) {
|
||||
this.results = [];
|
||||
this.listEl.empty();
|
||||
return;
|
||||
}
|
||||
this.listEl.innerHTML = '<div class="zhiyi-search-loading">搜索中…</div>';
|
||||
this.loading = true;
|
||||
try {
|
||||
const data = await searchMemories(query, "hermes-main");
|
||||
this.results = data.memories || [];
|
||||
this.renderResults();
|
||||
} catch (e) {
|
||||
this.listEl.innerHTML = `<div class="zhiyi-search-empty">搜索失败: ${(e as Error).message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
private renderResults() {
|
||||
this.listEl.empty();
|
||||
if (this.results.length === 0) {
|
||||
this.listEl.createEl("div", { cls: "zhiyi-search-empty", text: "未找到结果" });
|
||||
return;
|
||||
}
|
||||
for (const r of this.results) {
|
||||
const item = this.listEl.createEl("div", { cls: "zhiyi-search-result" });
|
||||
const hdr = item.createEl("div", { cls: "zhiyi-search-result-header" });
|
||||
hdr.createEl("span", { cls: "zhiyi-search-cat", text: r.category || "normal" });
|
||||
hdr.createEl("span", { cls: "zhiyi-search-score", text: ` relevance: ${(r.score ?? 0).toFixed(3)}` });
|
||||
item.createEl("div", { cls: "zhiyi-search-content", text: this.truncate(r.content, 200) });
|
||||
item.createEl("div", { cls: "zhiyi-search-id", text: r.id });
|
||||
item.onclick = () => this.selectResult(r);
|
||||
}
|
||||
}
|
||||
|
||||
private selectResult(r: { id: string; content: string }) {
|
||||
this.onSelect?.(r.id, r.content);
|
||||
this.close();
|
||||
}
|
||||
|
||||
private truncate(s: string, max: number) {
|
||||
if (!s) return "";
|
||||
return s.length > max ? s.slice(0, max) + "…" : s;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.listEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
|
||||
|
||||
// ─── API 客户端(调用织忆 Go API)─────────────────────────────────────────────
|
||||
|
||||
const API_BASE = "http://localhost:7821";
|
||||
const API_KEY = "zhiyi-dev-key-2026";
|
||||
|
||||
interface ApiOptions {
|
||||
method?: string;
|
||||
body?: unknown;
|
||||
}
|
||||
|
||||
async function apiCall<T = unknown>(
|
||||
path: string,
|
||||
opts: ApiOptions = {}
|
||||
): Promise<T> {
|
||||
const method = opts.method ?? "GET";
|
||||
const headers: Record<string, string> = {
|
||||
"X-API-Key": API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const init: RequestInit = { method, headers };
|
||||
if (opts.body !== undefined) {
|
||||
init.body = JSON.stringify(opts.body);
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, init);
|
||||
if (!res.ok) {
|
||||
throw new Error(`API ${path} failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
// 有些端点返回空 body
|
||||
const text = await res.text();
|
||||
if (!text) return {} as T;
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
// ─── 数据类型 ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface MemoryRecord {
|
||||
id: string;
|
||||
content: string;
|
||||
category: string;
|
||||
namespace: string;
|
||||
quality_score: number;
|
||||
created_at: string;
|
||||
agent_id?: string;
|
||||
}
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
label: string;
|
||||
category?: string;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
relation: string;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export interface GraphNavigateResult {
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
paths: Array<{ source: string; target: string; relation: string }>;
|
||||
}
|
||||
|
||||
export interface RecallResult {
|
||||
memories: Array<{ id: string; content: string; score: number; category: string }>;
|
||||
}
|
||||
|
||||
export interface DistillStatus {
|
||||
queue_len: number;
|
||||
daily_used: number;
|
||||
daily_limit: number;
|
||||
batch_size: number;
|
||||
last_distill?: string;
|
||||
}
|
||||
|
||||
export interface DistillQuota {
|
||||
remaining: number;
|
||||
used: number;
|
||||
limit: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ─── API 方法 ────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchStats() {
|
||||
return apiCall<{ total_memories: number; total_episodes: number; backend: string }>(
|
||||
"/api/v1/stats"
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchMemories(opts: {
|
||||
namespace?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{ memories: MemoryRecord[]; total: number }> {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.namespace) params.set("namespace", opts.namespace);
|
||||
if (opts.limit) params.set("limit", String(opts.limit));
|
||||
if (opts.offset !== undefined) params.set("offset", String(opts.offset));
|
||||
const qs = params.toString();
|
||||
return apiCall(`/api/v1/memories?${qs}`);
|
||||
}
|
||||
|
||||
export async function searchMemories(query: string, namespace = "hermes-main") {
|
||||
return apiCall<{ memories: RecallResult["memories"] }>("/api/v1/search/recall", {
|
||||
method: "POST",
|
||||
body: { query, namespace, top_k: 20 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchNeighbors(entity: string) {
|
||||
return apiCall<GraphNavigateResult>(`/api/v1/graph/navigate?entity=${encodeURIComponent(entity)}&depth=1`);
|
||||
}
|
||||
|
||||
export async function fetchGraphExport(namespace = "hermes-main", limit = 100) {
|
||||
return apiCall<{ nodes: GraphNode[]; edges: GraphEdge[] }>(
|
||||
`/api/v1/graph/export?namespace=${namespace}&limit=${limit}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchDistillStatus(): Promise<DistillStatus> {
|
||||
return apiCall("/api/v1/distill/status");
|
||||
}
|
||||
|
||||
export async function fetchDistillQuota(): Promise<DistillQuota> {
|
||||
return apiCall("/api/v1/distill/quota");
|
||||
}
|
||||
|
||||
export async function fetchPageRank(limit = 30) {
|
||||
return apiCall<{ pagerank: Array<{ entity: string; score: number }>; count: number }>(
|
||||
`/api/v1/graph/pagerank?limit=${limit}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchByEntity(entity: string) {
|
||||
return apiCall<{ memories: MemoryRecord[] }>(
|
||||
`/api/v1/memories/by-entity/${encodeURIComponent(entity)}`
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* 织忆插件全局样式 */
|
||||
|
||||
/* 侧边栏视图通用 */
|
||||
.workspace-leaf-content[data-type="zhiyi-memory-view"],
|
||||
.workspace-leaf-content[data-type="zhiyi-graph-view"] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 分页按钮 */
|
||||
.zhiyi-pagination button:hover:not(:disabled) {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
/* 搜索模态框覆盖层 */
|
||||
.zhiyi-search-modal .modal-bg {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.zhiyi-search-modal .modal-container {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 图谱 tooltip */
|
||||
.zhiyi-tooltip {
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 设置页 */
|
||||
.zhiyi-settings-section {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.zhiyi-settings-section h3 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
/* 蒸馏警告高亮 */
|
||||
.zhiyi-warn {
|
||||
color: #ff6b6b;
|
||||
font-weight: 700;
|
||||
}
|
||||
Loading…
Reference in New Issue