@hzttt/multimodal-rag 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -0
- package/index.ts +321 -0
- package/openclaw.plugin.json +114 -0
- package/package.json +49 -0
- package/src/embeddings.ts +139 -0
- package/src/processor.ts +123 -0
- package/src/setup.ts +214 -0
- package/src/storage.ts +375 -0
- package/src/tools.ts +629 -0
- package/src/types.ts +71 -0
- package/src/watcher.ts +464 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Multimodal RAG Plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw 多模态 RAG 插件 — 使用本地 AI 模型对图像和音频进行语义索引与时间感知搜索。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **图像索引**:使用 Qwen3-VL 自动描述图像内容并生成嵌入向量
|
|
8
|
+
- **音频索引**:使用 Whisper 转录音频并生成嵌入向量
|
|
9
|
+
- **语义搜索**:基于向量相似度的语义检索,支持中英文
|
|
10
|
+
- **时间过滤**:按文件创建时间范围过滤搜索结果
|
|
11
|
+
- **自动监听**:实时监听文件夹变化,自动索引新增文件
|
|
12
|
+
- **向量存储**:使用 LanceDB 高效存储和检索
|
|
13
|
+
- **智能去重**:基于文件 SHA256 哈希去重
|
|
14
|
+
|
|
15
|
+
## 前置条件
|
|
16
|
+
|
|
17
|
+
- [Ollama](https://ollama.ai) 已安装并运行
|
|
18
|
+
- 以下 Ollama 模型已拉取:
|
|
19
|
+
- `qwen3-vl:2b` (视觉模型,图像描述)
|
|
20
|
+
- `qwen3-embedding:latest` (嵌入模型,向量生成)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 安装模型
|
|
24
|
+
ollama pull qwen3-vl:2b
|
|
25
|
+
ollama pull qwen3-embedding:latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
### 方式一:从 npm 安装(推荐)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
openclaw plugins install @hzttt/multimodal-rag
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
插件会自动安装到 `~/.openclaw/extensions/multimodal-rag/`,并自动安装所有运行时依赖。
|
|
37
|
+
|
|
38
|
+
### 方式二:从 GitHub 安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
openclaw plugins install github:hzttt/multimodal-rag
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 方式三:从本地路径安装
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/hzttt/multimodal-rag.git
|
|
48
|
+
openclaw plugins install ./multimodal-rag
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 配置
|
|
52
|
+
|
|
53
|
+
### 交互式配置(推荐)
|
|
54
|
+
|
|
55
|
+
安装完成后,运行引导配置向导:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
openclaw multimodal-rag setup
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
向导将引导你配置**文件监听路径**,其他参数已使用推荐的默认值:
|
|
62
|
+
|
|
63
|
+
- **Ollama 地址**: `http://127.0.0.1:11434`
|
|
64
|
+
- **视觉模型**: `qwen3-vl:2b` (图像描述)
|
|
65
|
+
- **嵌入模型**: `qwen3-embedding:latest` (向量生成)
|
|
66
|
+
- **嵌入提供者**: `ollama` (本地)
|
|
67
|
+
- **数据库路径**: `/home/lucy/.openclaw/multimodal-rag.lance`
|
|
68
|
+
- **启动时索引**: `true` (自动索引已有文件)
|
|
69
|
+
|
|
70
|
+
你只需要指定要监听的文件夹路径即可。
|
|
71
|
+
|
|
72
|
+
### 手动配置
|
|
73
|
+
|
|
74
|
+
如需自定义配置,编辑 `~/.openclaw/openclaw.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"plugins": {
|
|
79
|
+
"entries": {
|
|
80
|
+
"multimodal-rag": {
|
|
81
|
+
"enabled": true,
|
|
82
|
+
"config": {
|
|
83
|
+
"watchPaths": ["~/mic-recordings", "/home/lucy/usb_data"],
|
|
84
|
+
"ollama": {
|
|
85
|
+
"baseUrl": "http://127.0.0.1:11434",
|
|
86
|
+
"visionModel": "qwen3-vl:2b",
|
|
87
|
+
"embedModel": "qwen3-embedding:latest"
|
|
88
|
+
},
|
|
89
|
+
"embedding": {
|
|
90
|
+
"provider": "ollama"
|
|
91
|
+
},
|
|
92
|
+
"dbPath": "/home/lucy/.openclaw/multimodal-rag.lance",
|
|
93
|
+
"indexExistingOnStart": true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 配置项说明
|
|
102
|
+
|
|
103
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
104
|
+
|--------|------|--------|------|
|
|
105
|
+
| `watchPaths` | string[] | `[]` | 监听的文件夹路径(支持 `~` 展开) |
|
|
106
|
+
| `ollama.baseUrl` | string | `http://127.0.0.1:11434` | Ollama 服务地址 |
|
|
107
|
+
| `ollama.visionModel` | string | `qwen3-vl:2b` | 用于图像描述的视觉模型 |
|
|
108
|
+
| `ollama.embedModel` | string | `qwen3-embedding:latest` | 用于生成嵌入向量的模型 |
|
|
109
|
+
| `embedding.provider` | string | `ollama` | 嵌入提供者: `ollama` 或 `openai` |
|
|
110
|
+
| `embedding.openaiApiKey` | string | - | OpenAI API Key(仅 openai 时需要) |
|
|
111
|
+
| `embedding.openaiModel` | string | `text-embedding-3-small` | OpenAI 嵌入模型 |
|
|
112
|
+
| `dbPath` | string | `~/.openclaw/multimodal-rag.lance` | LanceDB 数据库路径 |
|
|
113
|
+
| `watchDebounceMs` | number | `1000` | 文件监听去抖延迟(毫秒) |
|
|
114
|
+
| `indexExistingOnStart` | boolean | `true` | 启动时是否索引已有文件 |
|
|
115
|
+
|
|
116
|
+
配置完成后,重启 OpenClaw Gateway 使配置生效。
|
|
117
|
+
|
|
118
|
+
## 使用方法
|
|
119
|
+
|
|
120
|
+
### Agent 工具
|
|
121
|
+
|
|
122
|
+
插件注册 4 个 Agent 工具,可以在对话中自然地调用:
|
|
123
|
+
|
|
124
|
+
#### `media_search` — 语义搜索
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
用户:上周我去东方明珠拍的照片在哪
|
|
128
|
+
Agent:[调用 media_search] → 找到 3 张匹配的照片 → 发送给用户
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### `media_describe` — 获取媒体描述
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
用户:这个录音说了什么
|
|
135
|
+
Agent:[调用 media_describe(filePath)] → 返回音频转录内容
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### `media_list` — 浏览媒体文件
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
用户:列出最近的照片
|
|
142
|
+
Agent:[调用 media_list(type="image")] → 返回最近索引的图片列表
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `media_stats` — 查看库统计
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
用户:我的媒体库有多少文件
|
|
149
|
+
Agent:[调用 media_stats] → 总计 120 个文件,图片 80,音频 40
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### CLI 命令
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# 交互式配置
|
|
156
|
+
openclaw multimodal-rag setup
|
|
157
|
+
|
|
158
|
+
# 手动索引文件或文件夹
|
|
159
|
+
openclaw multimodal-rag index ~/Pictures/photo.jpg
|
|
160
|
+
|
|
161
|
+
# 语义搜索
|
|
162
|
+
openclaw multimodal-rag search "东方明珠"
|
|
163
|
+
openclaw multimodal-rag search "会议讨论" --type audio --after 2026-01-29
|
|
164
|
+
|
|
165
|
+
# 查看索引统计
|
|
166
|
+
openclaw multimodal-rag stats
|
|
167
|
+
|
|
168
|
+
# 列出已索引文件
|
|
169
|
+
openclaw multimodal-rag list --type image --limit 10
|
|
170
|
+
|
|
171
|
+
# 完整重新索引
|
|
172
|
+
openclaw multimodal-rag reindex --confirm
|
|
173
|
+
|
|
174
|
+
# 清空索引
|
|
175
|
+
openclaw multimodal-rag clear --confirm
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 故障排除
|
|
179
|
+
|
|
180
|
+
### Ollama 连接失败
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# 确保 Ollama 已启动
|
|
184
|
+
ollama serve
|
|
185
|
+
|
|
186
|
+
# 检查连接
|
|
187
|
+
curl http://127.0.0.1:11434/api/tags
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 嵌入维度不匹配
|
|
191
|
+
|
|
192
|
+
切换嵌入模型后需要重建索引:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
openclaw multimodal-rag reindex --confirm
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 文件监听不生效
|
|
199
|
+
|
|
200
|
+
检查路径是否正确,以及插件是否已启用:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
openclaw plugins list | grep multimodal-rag
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 许可证
|
|
207
|
+
|
|
208
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Multimodal RAG Plugin
|
|
3
|
+
*
|
|
4
|
+
* 多模态 RAG 插件,支持图像和音频的语义索引与时间感知搜索。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
8
|
+
import { MediaStorage } from "./src/storage.js";
|
|
9
|
+
import { createEmbeddingProvider } from "./src/embeddings.js";
|
|
10
|
+
import { createMediaProcessor } from "./src/processor.js";
|
|
11
|
+
import { MediaWatcher } from "./src/watcher.js";
|
|
12
|
+
import {
|
|
13
|
+
createMediaSearchTool,
|
|
14
|
+
createMediaDescribeTool,
|
|
15
|
+
createMediaListTool,
|
|
16
|
+
createMediaStatsTool,
|
|
17
|
+
} from "./src/tools.js";
|
|
18
|
+
import { runSetup } from "./src/setup.js";
|
|
19
|
+
import type { PluginConfig } from "./src/types.js";
|
|
20
|
+
|
|
21
|
+
const multimodalRagPlugin = {
|
|
22
|
+
id: "multimodal-rag",
|
|
23
|
+
name: "Multimodal RAG",
|
|
24
|
+
description:
|
|
25
|
+
"多模态 RAG 插件,支持图像和音频的语义索引与时间感知搜索",
|
|
26
|
+
kind: "rag" as const,
|
|
27
|
+
|
|
28
|
+
register(api: OpenClawPluginApi) {
|
|
29
|
+
// 解析配置(合并默认值)
|
|
30
|
+
const userConfig = (api.pluginConfig || {}) as Partial<PluginConfig>;
|
|
31
|
+
|
|
32
|
+
const cfg: PluginConfig = {
|
|
33
|
+
watchPaths: userConfig.watchPaths || [],
|
|
34
|
+
fileTypes: {
|
|
35
|
+
image: userConfig.fileTypes?.image || [".jpg", ".jpeg", ".png", ".webp", ".gif", ".heic"],
|
|
36
|
+
audio: userConfig.fileTypes?.audio || [".wav", ".mp3", ".m4a", ".ogg", ".flac", ".aac"],
|
|
37
|
+
},
|
|
38
|
+
ollama: {
|
|
39
|
+
baseUrl: userConfig.ollama?.baseUrl || "http://127.0.0.1:11434",
|
|
40
|
+
visionModel: userConfig.ollama?.visionModel || "qwen3-vl:2b",
|
|
41
|
+
embedModel: userConfig.ollama?.embedModel || "qwen3-embedding:latest",
|
|
42
|
+
},
|
|
43
|
+
embedding: {
|
|
44
|
+
provider: userConfig.embedding?.provider || "ollama",
|
|
45
|
+
openaiApiKey: userConfig.embedding?.openaiApiKey,
|
|
46
|
+
openaiModel: userConfig.embedding?.openaiModel || "text-embedding-3-small",
|
|
47
|
+
},
|
|
48
|
+
dbPath: userConfig.dbPath || "~/.openclaw/multimodal-rag.lance",
|
|
49
|
+
watchDebounceMs: userConfig.watchDebounceMs || 1000,
|
|
50
|
+
indexExistingOnStart: userConfig.indexExistingOnStart !== false,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 解析数据库路径
|
|
54
|
+
const resolvedDbPath = api.resolvePath(cfg.dbPath);
|
|
55
|
+
|
|
56
|
+
// 创建嵌入提供者
|
|
57
|
+
const embeddings = createEmbeddingProvider({
|
|
58
|
+
provider: cfg.embedding.provider,
|
|
59
|
+
ollamaBaseUrl: cfg.ollama.baseUrl,
|
|
60
|
+
ollamaModel: cfg.ollama.embedModel,
|
|
61
|
+
openaiApiKey: cfg.embedding.openaiApiKey,
|
|
62
|
+
openaiModel: cfg.embedding.openaiModel,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const vectorDim = embeddings.getDimension();
|
|
66
|
+
api.logger.info?.(
|
|
67
|
+
`multimodal-rag: Using ${cfg.embedding.provider} embeddings (dim=${vectorDim})`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// 创建存储
|
|
71
|
+
const storage = new MediaStorage(resolvedDbPath, vectorDim);
|
|
72
|
+
|
|
73
|
+
// 创建媒体处理器
|
|
74
|
+
const processor = createMediaProcessor({
|
|
75
|
+
ollamaBaseUrl: cfg.ollama.baseUrl,
|
|
76
|
+
visionModel: cfg.ollama.visionModel,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 创建文件监听器
|
|
80
|
+
const watcher = new MediaWatcher(cfg, storage, embeddings, processor, api.logger);
|
|
81
|
+
|
|
82
|
+
// ========================================================================
|
|
83
|
+
// 注册工具
|
|
84
|
+
// ========================================================================
|
|
85
|
+
|
|
86
|
+
// 1. 统计工具 - 让 Agent 了解媒体库状态
|
|
87
|
+
api.registerTool(createMediaStatsTool(storage, watcher), {
|
|
88
|
+
name: "media_stats",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 2. 搜索工具 - 主要的内容查找工具
|
|
92
|
+
api.registerTool(createMediaSearchTool(storage, embeddings), {
|
|
93
|
+
name: "media_search",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 3. 列表工具 - 浏览和按时间过滤
|
|
97
|
+
api.registerTool(createMediaListTool(storage, cfg), {
|
|
98
|
+
name: "media_list",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 4. 描述工具 - 查看单个文件详情
|
|
102
|
+
api.registerTool(createMediaDescribeTool(storage, processor, embeddings, watcher), {
|
|
103
|
+
name: "media_describe",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
api.logger.info?.("multimodal-rag: Registered 4 agent tools");
|
|
107
|
+
|
|
108
|
+
// ========================================================================
|
|
109
|
+
// 注册 CLI 命令
|
|
110
|
+
// ========================================================================
|
|
111
|
+
|
|
112
|
+
api.registerCli(({ program }) => {
|
|
113
|
+
const rag = program
|
|
114
|
+
.command("multimodal-rag")
|
|
115
|
+
.description("Multimodal RAG plugin commands");
|
|
116
|
+
|
|
117
|
+
// openclaw multimodal-rag index <path>
|
|
118
|
+
rag
|
|
119
|
+
.command("index")
|
|
120
|
+
.description("手动索引指定路径的媒体文件")
|
|
121
|
+
.argument("<path>", "文件或文件夹路径")
|
|
122
|
+
.action(async (path: string) => {
|
|
123
|
+
try {
|
|
124
|
+
await watcher.indexPath(path);
|
|
125
|
+
console.log(`✓ 索引完成: ${path}`);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`✗ 索引失败: ${String(error)}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// openclaw multimodal-rag search <query>
|
|
133
|
+
rag
|
|
134
|
+
.command("search")
|
|
135
|
+
.description("搜索媒体文件")
|
|
136
|
+
.argument("<query>", "搜索查询")
|
|
137
|
+
.option("--type <type>", "媒体类型: image, audio, all", "all")
|
|
138
|
+
.option("--after <date>", "开始时间 (ISO 格式)")
|
|
139
|
+
.option("--before <date>", "结束时间 (ISO 格式)")
|
|
140
|
+
.option("--limit <n>", "返回数量", "5")
|
|
141
|
+
.action(async (query: string, opts: any) => {
|
|
142
|
+
try {
|
|
143
|
+
const vector = await embeddings.embed(query);
|
|
144
|
+
const afterTs = opts.after ? new Date(opts.after).getTime() : undefined;
|
|
145
|
+
const beforeTs = opts.before ? new Date(opts.before).getTime() : undefined;
|
|
146
|
+
|
|
147
|
+
const results = await storage.search(vector, {
|
|
148
|
+
type: opts.type,
|
|
149
|
+
after: afterTs,
|
|
150
|
+
before: beforeTs,
|
|
151
|
+
limit: Number.parseInt(opts.limit),
|
|
152
|
+
minScore: 0.3,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (results.length === 0) {
|
|
156
|
+
console.log("未找到相关媒体文件");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`找到 ${results.length} 个相关媒体文件:\n`);
|
|
161
|
+
for (const r of results) {
|
|
162
|
+
const date = new Date(r.entry.fileCreatedAt).toLocaleString("zh-CN");
|
|
163
|
+
const score = (r.score * 100).toFixed(0);
|
|
164
|
+
console.log(`[${r.entry.fileType}] ${r.entry.fileName} (${score}%)`);
|
|
165
|
+
console.log(` 路径: ${r.entry.filePath}`);
|
|
166
|
+
console.log(` 时间: ${date}`);
|
|
167
|
+
console.log(` 描述: ${r.entry.description.slice(0, 100)}...\n`);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(`搜索失败: ${String(error)}`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// openclaw multimodal-rag stats
|
|
176
|
+
rag
|
|
177
|
+
.command("stats")
|
|
178
|
+
.description("显示索引统计")
|
|
179
|
+
.action(async () => {
|
|
180
|
+
try {
|
|
181
|
+
// 使用 count() 统一查询逻辑(全量扫描 + 内存过滤)
|
|
182
|
+
const total = await storage.count();
|
|
183
|
+
const imageCount = await storage.count("image");
|
|
184
|
+
const audioCount = await storage.count("audio");
|
|
185
|
+
|
|
186
|
+
console.log("媒体库统计:");
|
|
187
|
+
console.log(` 总计: ${total} 个文件`);
|
|
188
|
+
console.log(` 图片: ${imageCount} 个`);
|
|
189
|
+
console.log(` 音频: ${audioCount} 个`);
|
|
190
|
+
|
|
191
|
+
// 数据完整性检查
|
|
192
|
+
if (total !== imageCount + audioCount) {
|
|
193
|
+
console.log(` 警告: 总数不匹配 (${total} ≠ ${imageCount} + ${audioCount})`);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(`统计失败: ${String(error)}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// openclaw multimodal-rag list
|
|
202
|
+
rag
|
|
203
|
+
.command("list")
|
|
204
|
+
.description("列出已索引的媒体文件")
|
|
205
|
+
.option("--type <type>", "媒体类型: image, audio, all", "all")
|
|
206
|
+
.option("--after <date>", "开始时间 (ISO 格式)")
|
|
207
|
+
.option("--before <date>", "结束时间 (ISO 格式)")
|
|
208
|
+
.option("--limit <n>", "返回数量", "20")
|
|
209
|
+
.option("--offset <n>", "偏移量", "0")
|
|
210
|
+
.action(async (opts: any) => {
|
|
211
|
+
try {
|
|
212
|
+
const afterTs = opts.after ? new Date(opts.after).getTime() : undefined;
|
|
213
|
+
const beforeTs = opts.before ? new Date(opts.before).getTime() : undefined;
|
|
214
|
+
|
|
215
|
+
const { total, entries } = await storage.list({
|
|
216
|
+
type: opts.type,
|
|
217
|
+
after: afterTs,
|
|
218
|
+
before: beforeTs,
|
|
219
|
+
limit: Number.parseInt(opts.limit),
|
|
220
|
+
offset: Number.parseInt(opts.offset),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (entries.length === 0) {
|
|
224
|
+
console.log("没有找到符合条件的媒体文件");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(`已索引 ${total} 个媒体文件:\n`);
|
|
229
|
+
for (let i = 0; i < entries.length; i++) {
|
|
230
|
+
const e = entries[i];
|
|
231
|
+
const date = new Date(e.fileCreatedAt).toLocaleString("zh-CN");
|
|
232
|
+
console.log(`${opts.offset + i + 1}. [${e.fileType}] ${e.fileName}`);
|
|
233
|
+
console.log(` 路径: ${e.filePath}`);
|
|
234
|
+
console.log(` 时间: ${date}`);
|
|
235
|
+
console.log(` 描述: ${e.description.slice(0, 80)}${e.description.length > 80 ? "..." : ""}\n`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (total > opts.offset + entries.length) {
|
|
239
|
+
console.log(`(显示 ${opts.offset + 1}-${opts.offset + entries.length},共 ${total} 个)`);
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`列表失败: ${String(error)}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// openclaw multimodal-rag clear
|
|
248
|
+
rag
|
|
249
|
+
.command("clear")
|
|
250
|
+
.description("清空索引(谨慎使用)")
|
|
251
|
+
.option("--confirm", "确认清空")
|
|
252
|
+
.action(async (opts: any) => {
|
|
253
|
+
if (!opts.confirm) {
|
|
254
|
+
console.error("请使用 --confirm 确认清空操作");
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await storage.clear();
|
|
260
|
+
console.log("✓ 索引已清空");
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error(`清空失败: ${String(error)}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// openclaw multimodal-rag reindex
|
|
268
|
+
rag
|
|
269
|
+
.command("reindex")
|
|
270
|
+
.description("完整重新索引(清空数据库并重新扫描所有文件)")
|
|
271
|
+
.option("--confirm", "确认重新索引")
|
|
272
|
+
.action(async (opts: any) => {
|
|
273
|
+
if (!opts.confirm) {
|
|
274
|
+
console.error("请使用 --confirm 确认重新索引操作");
|
|
275
|
+
console.error("警告: 此操作会清空现有索引并重新扫描所有文件");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
console.log("开始完整重新索引...");
|
|
281
|
+
await watcher.reindexAll();
|
|
282
|
+
console.log("✓ 重新索引完成");
|
|
283
|
+
console.log("提示: 使用 'openclaw multimodal-rag stats' 查看进度");
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error(`重新索引失败: ${String(error)}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// openclaw multimodal-rag setup
|
|
291
|
+
rag
|
|
292
|
+
.command("setup")
|
|
293
|
+
.description("交互式引导配置插件")
|
|
294
|
+
.action(async () => {
|
|
295
|
+
await runSetup();
|
|
296
|
+
});
|
|
297
|
+
}, { commands: ["multimodal-rag"] });
|
|
298
|
+
|
|
299
|
+
// ========================================================================
|
|
300
|
+
// 注册服务(文件监听)
|
|
301
|
+
// ========================================================================
|
|
302
|
+
|
|
303
|
+
api.registerService({
|
|
304
|
+
id: "multimodal-rag-watcher",
|
|
305
|
+
start: async () => {
|
|
306
|
+
await watcher.start();
|
|
307
|
+
api.logger.info?.("multimodal-rag: File watcher started");
|
|
308
|
+
},
|
|
309
|
+
stop: async () => {
|
|
310
|
+
await watcher.stop();
|
|
311
|
+
api.logger.info?.("multimodal-rag: File watcher stopped");
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
api.logger.info?.(
|
|
316
|
+
`multimodal-rag: Plugin initialized (db: ${resolvedDbPath})`,
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export default multimodalRagPlugin;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "multimodal-rag",
|
|
3
|
+
"name": "Multimodal RAG",
|
|
4
|
+
"description": "多模态 RAG 插件,使用本地 AI 模型对图像和音频进行语义索引与时间感知搜索",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"kind": "rag",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"watchPaths": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": { "type": "string" },
|
|
13
|
+
"default": [],
|
|
14
|
+
"description": "监听的文件夹路径列表(支持 ~ 展开)"
|
|
15
|
+
},
|
|
16
|
+
"fileTypes": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"image": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": { "type": "string" },
|
|
22
|
+
"default": [".jpg", ".jpeg", ".png", ".webp", ".gif", ".heic"]
|
|
23
|
+
},
|
|
24
|
+
"audio": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": { "type": "string" },
|
|
27
|
+
"default": [".wav", ".mp3", ".m4a", ".ogg", ".flac", ".aac"]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"default": {}
|
|
31
|
+
},
|
|
32
|
+
"ollama": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"baseUrl": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": "http://127.0.0.1:11434"
|
|
38
|
+
},
|
|
39
|
+
"visionModel": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"default": "qwen3-vl:2b",
|
|
42
|
+
"description": "用于图像描述的视觉模型"
|
|
43
|
+
},
|
|
44
|
+
"embedModel": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"default": "qwen3-embedding:latest",
|
|
47
|
+
"description": "用于生成嵌入向量的模型"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"default": {}
|
|
51
|
+
},
|
|
52
|
+
"embedding": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"provider": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": ["ollama", "openai"],
|
|
58
|
+
"default": "ollama"
|
|
59
|
+
},
|
|
60
|
+
"openaiApiKey": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "OpenAI API Key(仅当 provider=openai 时需要)"
|
|
63
|
+
},
|
|
64
|
+
"openaiModel": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"default": "text-embedding-3-small"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"default": {}
|
|
70
|
+
},
|
|
71
|
+
"dbPath": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"default": "~/.openclaw/multimodal-rag.lance",
|
|
74
|
+
"description": "LanceDB 数据库路径"
|
|
75
|
+
},
|
|
76
|
+
"watchDebounceMs": {
|
|
77
|
+
"type": "number",
|
|
78
|
+
"default": 1000,
|
|
79
|
+
"description": "文件监听去抖延迟(毫秒)"
|
|
80
|
+
},
|
|
81
|
+
"indexExistingOnStart": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"default": true,
|
|
84
|
+
"description": "启动时是否索引现有文件"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"uiHints": {
|
|
89
|
+
"watchPaths": {
|
|
90
|
+
"label": "监听路径",
|
|
91
|
+
"placeholder": "~/mic-recordings"
|
|
92
|
+
},
|
|
93
|
+
"ollama.visionModel": {
|
|
94
|
+
"label": "视觉模型",
|
|
95
|
+
"placeholder": "qwen3-vl:2b"
|
|
96
|
+
},
|
|
97
|
+
"ollama.embedModel": {
|
|
98
|
+
"label": "嵌入模型",
|
|
99
|
+
"placeholder": "qwen3-embedding:latest"
|
|
100
|
+
},
|
|
101
|
+
"embedding.openaiApiKey": {
|
|
102
|
+
"label": "OpenAI API Key",
|
|
103
|
+
"sensitive": true
|
|
104
|
+
},
|
|
105
|
+
"dbPath": {
|
|
106
|
+
"label": "数据库路径",
|
|
107
|
+
"advanced": true
|
|
108
|
+
},
|
|
109
|
+
"watchDebounceMs": {
|
|
110
|
+
"label": "去抖延迟(毫秒)",
|
|
111
|
+
"advanced": true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hzttt/multimodal-rag",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "OpenClaw plugin for multimodal RAG - semantic indexing and time-aware search for images and audio using local AI models",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/hzttt/multimodal-rag.git"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "node test/test-embedding.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"openclaw",
|
|
16
|
+
"plugin",
|
|
17
|
+
"rag",
|
|
18
|
+
"multimodal",
|
|
19
|
+
"vision",
|
|
20
|
+
"embedding",
|
|
21
|
+
"lancedb",
|
|
22
|
+
"ollama",
|
|
23
|
+
"whisper"
|
|
24
|
+
],
|
|
25
|
+
"author": "hzttt",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"files": [
|
|
28
|
+
"index.ts",
|
|
29
|
+
"src/",
|
|
30
|
+
"openclaw.plugin.json",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@lancedb/lancedb": "^0.14.0",
|
|
35
|
+
"@sinclair/typebox": "^0.33.0",
|
|
36
|
+
"chokidar": "^4.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"openclaw": "*"
|
|
40
|
+
},
|
|
41
|
+
"openclaw": {
|
|
42
|
+
"extensions": [
|
|
43
|
+
"./index.ts"
|
|
44
|
+
],
|
|
45
|
+
"install": {
|
|
46
|
+
"npmSpec": "@hzttt/multimodal-rag"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|