@hzttt/multimodal-rag 0.2.7 → 0.2.9
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 +15 -9
- package/index.ts +2 -0
- package/openclaw.plugin.json +24 -4
- package/package.json +1 -1
- package/src/notifier.ts +146 -3
- package/src/setup.ts +10 -0
- package/src/types.ts +2 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ OpenClaw 多模态 RAG 插件 — 使用本地 AI 模型对图像和音频进行
|
|
|
11
11
|
- **自动监听**:实时监听文件夹变化,自动索引新增文件
|
|
12
12
|
- **向量存储**:使用 LanceDB 高效存储和检索
|
|
13
13
|
- **智能去重**:基于文件 SHA256 哈希去重
|
|
14
|
-
-
|
|
14
|
+
- **索引通知**:批次聚合索引事件,默认唤醒 agent 生成并回复通知(可切换为 `message send` 直发)
|
|
15
15
|
|
|
16
16
|
## 前置条件
|
|
17
17
|
|
|
@@ -157,7 +157,7 @@ openclaw multimodal-rag setup -n \
|
|
|
157
157
|
- 非交互式执行时,`--notify-enabled` 仅负责“显式开启”,未传不会主动关闭通知。
|
|
158
158
|
- 当前没有 `--notify-disabled` 选项;要关闭通知请手动编辑 `~/.openclaw/openclaw.json`。
|
|
159
159
|
- `--notify-quiet-window` 和 `--notify-batch-timeout` 未传时会优先沿用已有配置,没有已有值才回退默认值。
|
|
160
|
-
- `notifications.channel` / `notifications.to` / `notifications.targets` 目前通过配置文件手动设置(setup 命令暂未提供对应参数)。
|
|
160
|
+
- `notifications.mode` / `notifications.agentId` / `notifications.channel` / `notifications.to` / `notifications.targets` 目前通过配置文件手动设置(setup 命令暂未提供对应参数)。
|
|
161
161
|
- 未配置 `targets` 且未配置 `channel+to` 时,插件会自动从 session store 选取“最近活跃会话”的 `lastChannel + lastTo` 作为通知目标。
|
|
162
162
|
|
|
163
163
|
### 手动配置
|
|
@@ -184,6 +184,8 @@ openclaw multimodal-rag setup -n \
|
|
|
184
184
|
"indexExistingOnStart": true,
|
|
185
185
|
"notifications": {
|
|
186
186
|
"enabled": true,
|
|
187
|
+
"mode": "agent",
|
|
188
|
+
"agentId": "main",
|
|
187
189
|
"quietWindowMs": 30000,
|
|
188
190
|
"batchTimeoutMs": 600000,
|
|
189
191
|
"channel": "last"
|
|
@@ -210,12 +212,14 @@ openclaw multimodal-rag setup -n \
|
|
|
210
212
|
| `dbPath` | string | `~/.openclaw/multimodal-rag.lance` | LanceDB 数据库路径 |
|
|
211
213
|
| `watchDebounceMs` | number | `1000` | 文件监听去抖延迟(毫秒) |
|
|
212
214
|
| `indexExistingOnStart` | boolean | `true` | 启动时是否索引已有文件 |
|
|
213
|
-
| `notifications.enabled` | boolean | `false` |
|
|
215
|
+
| `notifications.enabled` | boolean | `false` | 启用索引完成通知(默认唤醒 agent 回复) |
|
|
216
|
+
| `notifications.mode` | string | `"agent"` | 通知模式:`agent`(默认)/`message-send`/`auto` |
|
|
217
|
+
| `notifications.agentId` | string | `"main"` | 通知触发使用的 agent ID(仅 `agent/auto`,会沿用该 agent 的性格/身份设定) |
|
|
214
218
|
| `notifications.quietWindowMs` | number | `30000` | 静默窗口:最后一个文件处理完后等待多久再发送总结(毫秒) |
|
|
215
219
|
| `notifications.batchTimeoutMs` | number | `600000` | 批次最大超时:超过此时间强制发送总结(毫秒),防止大批量索引时等太久 |
|
|
216
|
-
| `notifications.channel` | string | `"last"` |
|
|
217
|
-
| `notifications.to` | string | - |
|
|
218
|
-
| `notifications.targets` | object[] | `[]` |
|
|
220
|
+
| `notifications.channel` | string | `"last"` | 回退链路渠道(未配置 `targets` 时使用) |
|
|
221
|
+
| `notifications.to` | string | - | 回退链路目标(未配置 `targets` 时使用) |
|
|
222
|
+
| `notifications.targets` | object[] | `[]` | 通知目标列表(agent 模式用于 reply-channel/reply-to) |
|
|
219
223
|
|
|
220
224
|
|
|
221
225
|
配置完成后,重启 OpenClaw Gateway 使配置生效。
|
|
@@ -228,9 +232,9 @@ openclaw multimodal-rag setup -n \
|
|
|
228
232
|
|
|
229
233
|
#### 工作原理
|
|
230
234
|
|
|
231
|
-
1.
|
|
235
|
+
1. **开始通知**:检测到第一个新文件时,插件默认用 `openclaw agent --deliver` 唤醒 agent 生成回复并发送
|
|
232
236
|
2. **批次聚合**:持续聚合多个文件的索引状态,避免频繁通知
|
|
233
|
-
3.
|
|
237
|
+
3. **完成通知**:所有文件处理完成后(静默窗口到期),插件再次触发 agent 发送总结
|
|
234
238
|
|
|
235
239
|
#### 通知示例
|
|
236
240
|
|
|
@@ -279,6 +283,8 @@ openclaw multimodal-rag setup -n \
|
|
|
279
283
|
"config": {
|
|
280
284
|
"notifications": {
|
|
281
285
|
"enabled": true,
|
|
286
|
+
"mode": "agent",
|
|
287
|
+
"agentId": "main",
|
|
282
288
|
"quietWindowMs": 30000,
|
|
283
289
|
"batchTimeoutMs": 600000,
|
|
284
290
|
"targets": [
|
|
@@ -298,7 +304,7 @@ openclaw multimodal-rag setup -n \
|
|
|
298
304
|
}
|
|
299
305
|
}
|
|
300
306
|
```
|
|
301
|
-
如果未配置 `notifications.targets`,插件会先尝试 `channel+to
|
|
307
|
+
如果未配置 `notifications.targets`,插件会先尝试 `channel+to`;若仍未配置,则自动使用最新活跃会话目标;都不可用时会回退到 hooks/system-event 兼容链路。
|
|
302
308
|
|
|
303
309
|
### Agent 工具
|
|
304
310
|
|
package/index.ts
CHANGED
|
@@ -51,6 +51,8 @@ const multimodalRagPlugin = {
|
|
|
51
51
|
indexExistingOnStart: userConfig.indexExistingOnStart !== false,
|
|
52
52
|
notifications: {
|
|
53
53
|
enabled: userConfig.notifications?.enabled ?? false,
|
|
54
|
+
mode: userConfig.notifications?.mode || "agent",
|
|
55
|
+
agentId: userConfig.notifications?.agentId,
|
|
54
56
|
quietWindowMs: userConfig.notifications?.quietWindowMs ?? 30000,
|
|
55
57
|
batchTimeoutMs: userConfig.notifications?.batchTimeoutMs ?? 600000,
|
|
56
58
|
channel: userConfig.notifications?.channel || "last",
|
package/openclaw.plugin.json
CHANGED
|
@@ -89,7 +89,17 @@
|
|
|
89
89
|
"enabled": {
|
|
90
90
|
"type": "boolean",
|
|
91
91
|
"default": false,
|
|
92
|
-
"description": "
|
|
92
|
+
"description": "启用索引通知(默认唤醒 agent 回复)"
|
|
93
|
+
},
|
|
94
|
+
"mode": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"enum": ["agent", "message-send", "auto"],
|
|
97
|
+
"default": "agent",
|
|
98
|
+
"description": "通知模式:agent(唤醒 agent 回复) / message-send(插件直发) / auto(先 agent 后直发)"
|
|
99
|
+
},
|
|
100
|
+
"agentId": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "mode=agent/auto 时用于触发通知回复的 agent ID(默认 main)"
|
|
93
103
|
},
|
|
94
104
|
"quietWindowMs": {
|
|
95
105
|
"type": "number",
|
|
@@ -112,7 +122,7 @@
|
|
|
112
122
|
},
|
|
113
123
|
"targets": {
|
|
114
124
|
"type": "array",
|
|
115
|
-
"description": "
|
|
125
|
+
"description": "通知目标列表(agent 模式用于 reply-channel/reply-to,message-send 模式用于直发)",
|
|
116
126
|
"items": {
|
|
117
127
|
"type": "object",
|
|
118
128
|
"properties": {
|
|
@@ -163,7 +173,17 @@
|
|
|
163
173
|
},
|
|
164
174
|
"notifications.enabled": {
|
|
165
175
|
"label": "启用通知",
|
|
166
|
-
"help": "
|
|
176
|
+
"help": "索引开始/完成时触发通知"
|
|
177
|
+
},
|
|
178
|
+
"notifications.mode": {
|
|
179
|
+
"label": "通知模式",
|
|
180
|
+
"advanced": true,
|
|
181
|
+
"help": "默认用 agent 生成并回复消息;可切到 message-send 直发"
|
|
182
|
+
},
|
|
183
|
+
"notifications.agentId": {
|
|
184
|
+
"label": "通知 Agent ID",
|
|
185
|
+
"advanced": true,
|
|
186
|
+
"help": "用于触发通知回复的 agent(默认 main)"
|
|
167
187
|
},
|
|
168
188
|
"notifications.quietWindowMs": {
|
|
169
189
|
"label": "静默窗口(毫秒)",
|
|
@@ -188,7 +208,7 @@
|
|
|
188
208
|
"notifications.targets": {
|
|
189
209
|
"label": "直发目标列表",
|
|
190
210
|
"advanced": true,
|
|
191
|
-
"help": "
|
|
211
|
+
"help": "通知目标列表;agent 模式会作为回复投递目标使用"
|
|
192
212
|
}
|
|
193
213
|
}
|
|
194
214
|
}
|
package/package.json
CHANGED
package/src/notifier.ts
CHANGED
|
@@ -19,6 +19,7 @@ type NotificationTargetResolved = {
|
|
|
19
19
|
to: string;
|
|
20
20
|
accountId?: string;
|
|
21
21
|
};
|
|
22
|
+
type NotificationDispatchMode = "agent" | "message-send" | "auto";
|
|
22
23
|
type HookAgentPayload = {
|
|
23
24
|
message: string;
|
|
24
25
|
name: string;
|
|
@@ -178,7 +179,7 @@ export class IndexNotifier implements IndexEventCallbacks {
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
/**
|
|
181
|
-
*
|
|
182
|
+
* 触发通知:优先级由 notifications.mode 决定,最后回退到 hooks/system-event。
|
|
182
183
|
*/
|
|
183
184
|
private triggerAgent(text: string): void {
|
|
184
185
|
void this.triggerAgentInternal(text);
|
|
@@ -188,8 +189,25 @@ export class IndexNotifier implements IndexEventCallbacks {
|
|
|
188
189
|
const system = this.runtime.system as unknown as RuntimeSystemCompat;
|
|
189
190
|
|
|
190
191
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
const mode = this.resolveDispatchMode();
|
|
193
|
+
if (mode === "agent") {
|
|
194
|
+
if (await this.dispatchViaAgentTurn(text, system)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (await this.dispatchViaMessageSend(text, system)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
} else if (mode === "message-send") {
|
|
201
|
+
if (await this.dispatchViaMessageSend(text, system)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
if (await this.dispatchViaAgentTurn(text, system)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (await this.dispatchViaMessageSend(text, system)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
193
211
|
}
|
|
194
212
|
|
|
195
213
|
if (await this.dispatchViaHookAgent(text)) {
|
|
@@ -228,6 +246,59 @@ export class IndexNotifier implements IndexEventCallbacks {
|
|
|
228
246
|
}
|
|
229
247
|
}
|
|
230
248
|
|
|
249
|
+
private resolveDispatchMode(): NotificationDispatchMode {
|
|
250
|
+
const rawMode = typeof this.config.mode === "string" ? this.config.mode.trim() : "";
|
|
251
|
+
if (rawMode === "agent" || rawMode === "message-send" || rawMode === "auto") {
|
|
252
|
+
return rawMode;
|
|
253
|
+
}
|
|
254
|
+
return "agent";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 通过 `openclaw agent --deliver` 触发 agent 主动回复用户。
|
|
259
|
+
* 这条路径会让最终消息由 agent 生成并投递,而不是插件直接发送文本。
|
|
260
|
+
*/
|
|
261
|
+
private async dispatchViaAgentTurn(
|
|
262
|
+
text: string,
|
|
263
|
+
system: RuntimeSystemCompat,
|
|
264
|
+
): Promise<boolean> {
|
|
265
|
+
if (typeof system.runCommandWithTimeout !== "function") {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const targets = await this.resolveNotificationTargets();
|
|
270
|
+
const deliveryTargets: Array<NotificationTargetResolved | undefined> =
|
|
271
|
+
targets.length > 0 ? targets : [undefined];
|
|
272
|
+
|
|
273
|
+
let successCount = 0;
|
|
274
|
+
for (const target of deliveryTargets) {
|
|
275
|
+
const argv = this.buildAgentNotifyCommand(text, target);
|
|
276
|
+
const targetLabel = this.formatTargetLabel(target);
|
|
277
|
+
try {
|
|
278
|
+
const result = await system.runCommandWithTimeout(argv, { timeoutMs: 180000 });
|
|
279
|
+
if (result.code === 0) {
|
|
280
|
+
successCount += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const detail = (result.stderr || result.stdout || "no output").trim();
|
|
284
|
+
this.logger.warn?.(
|
|
285
|
+
`Notification agent trigger failed (${targetLabel}, code=${String(result.code)}): ${detail}`,
|
|
286
|
+
);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
this.logger.warn?.(`Notification agent trigger error (${targetLabel}): ${String(err)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (successCount > 0) {
|
|
293
|
+
this.logger.info?.(
|
|
294
|
+
`Notification delivered via agent turns (${successCount}/${deliveryTargets.length}): ${text.slice(0, 80)}...`,
|
|
295
|
+
);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
231
302
|
/**
|
|
232
303
|
* 直发路径:通过 `openclaw message send` 向显式配置的目标推送通知。
|
|
233
304
|
* 不依赖 heartbeat,也不依赖 HTTP hooks。
|
|
@@ -490,6 +561,78 @@ export class IndexNotifier implements IndexEventCallbacks {
|
|
|
490
561
|
);
|
|
491
562
|
}
|
|
492
563
|
|
|
564
|
+
private resolveNotificationAgentId(): string {
|
|
565
|
+
const configuredAgentId =
|
|
566
|
+
typeof this.config.agentId === "string" ? this.config.agentId.trim() : "";
|
|
567
|
+
if (configuredAgentId) {
|
|
568
|
+
return configuredAgentId;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const agents = this.mainSessionConfig?.agents?.list ?? [];
|
|
572
|
+
return (
|
|
573
|
+
agents.find((agent) => agent.default && typeof agent.id === "string" && agent.id.trim())
|
|
574
|
+
?.id?.trim() ??
|
|
575
|
+
agents.find((agent) => typeof agent.id === "string" && agent.id.trim())?.id?.trim() ??
|
|
576
|
+
"main"
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private buildAgentNotifyCommand(
|
|
581
|
+
text: string,
|
|
582
|
+
target: NotificationTargetResolved | undefined,
|
|
583
|
+
): string[] {
|
|
584
|
+
const nodeExec = process.execPath;
|
|
585
|
+
const cliEntry = process.argv[1];
|
|
586
|
+
const agentId = this.resolveNotificationAgentId();
|
|
587
|
+
const argvCore = [
|
|
588
|
+
"agent",
|
|
589
|
+
"--agent",
|
|
590
|
+
agentId,
|
|
591
|
+
"--message",
|
|
592
|
+
this.buildAgentPrompt(text),
|
|
593
|
+
"--thinking",
|
|
594
|
+
"low",
|
|
595
|
+
"--deliver",
|
|
596
|
+
"--timeout",
|
|
597
|
+
"120",
|
|
598
|
+
"--reply-channel",
|
|
599
|
+
target?.channel ?? "last",
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
if (target?.to) {
|
|
603
|
+
argvCore.push("--reply-to", target.to);
|
|
604
|
+
}
|
|
605
|
+
if (typeof target?.accountId === "string" && target.accountId.length > 0) {
|
|
606
|
+
argvCore.push("--reply-account", target.accountId);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (typeof cliEntry === "string" && cliEntry.trim().length > 0) {
|
|
610
|
+
return [nodeExec, cliEntry, ...argvCore];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return ["openclaw", ...argvCore];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private buildAgentPrompt(text: string): string {
|
|
617
|
+
return [
|
|
618
|
+
"你收到一条来自 Multimodal RAG 的索引事件通知。",
|
|
619
|
+
"请严格按你当前 agent 的人格设定回复,优先遵循已注入的 IDENTITY.md / SOUL.md。",
|
|
620
|
+
"如果本轮上下文里看不到 IDENTITY.md 或 SOUL.md,请先读取工作区对应文件(存在就读),再回复。",
|
|
621
|
+
"禁止调用 message、sessions_send、sessions_spawn 等消息投递工具;系统会用 --deliver 自动投递。",
|
|
622
|
+
"除非为了读取 IDENTITY.md / SOUL.md,否则不要调用其他工具。",
|
|
623
|
+
"只输出最终发给用户的通知正文,不要输出“已发送/已通知/处理中”等过程状态。",
|
|
624
|
+
"避免模板化官腔,用该 agent 一贯口吻写一段自然中文通知。",
|
|
625
|
+
`事件内容:${text}`,
|
|
626
|
+
].join("\n");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private formatTargetLabel(target?: NotificationTargetResolved): string {
|
|
630
|
+
if (!target) {
|
|
631
|
+
return "last";
|
|
632
|
+
}
|
|
633
|
+
return `${target.channel}:${target.to}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
493
636
|
/**
|
|
494
637
|
* CLI 回退:兼容缺少 requestHeartbeatNow 的旧版 runtime。
|
|
495
638
|
*/
|
package/src/setup.ts
CHANGED
|
@@ -31,6 +31,8 @@ type PluginConfigPartial = {
|
|
|
31
31
|
indexExistingOnStart?: boolean;
|
|
32
32
|
notifications?: {
|
|
33
33
|
enabled?: boolean;
|
|
34
|
+
mode?: "agent" | "message-send" | "auto";
|
|
35
|
+
agentId?: string;
|
|
34
36
|
quietWindowMs?: number;
|
|
35
37
|
batchTimeoutMs?: number;
|
|
36
38
|
channel?: string;
|
|
@@ -137,6 +139,8 @@ export async function runNonInteractiveSetup(opts: NonInteractiveSetupOpts): Pro
|
|
|
137
139
|
notifications: {
|
|
138
140
|
...existing.notifications,
|
|
139
141
|
enabled: opts.notifyEnabled ?? existing.notifications?.enabled ?? false,
|
|
142
|
+
mode: existing.notifications?.mode ?? "agent",
|
|
143
|
+
agentId: existing.notifications?.agentId,
|
|
140
144
|
quietWindowMs: opts.notifyQuietWindowMs ?? existing.notifications?.quietWindowMs ?? 30000,
|
|
141
145
|
batchTimeoutMs: opts.notifyBatchTimeoutMs ?? existing.notifications?.batchTimeoutMs ?? 600000,
|
|
142
146
|
channel: existing.notifications?.channel ?? "last",
|
|
@@ -158,6 +162,10 @@ export async function runNonInteractiveSetup(opts: NonInteractiveSetupOpts): Pro
|
|
|
158
162
|
console.log(` 启动时索引: ${pluginConfig.indexExistingOnStart ? "是" : "否"}`);
|
|
159
163
|
console.log(` 索引通知: ${pluginConfig.notifications!.enabled ? "已启用" : "已禁用"}`);
|
|
160
164
|
if (pluginConfig.notifications!.enabled) {
|
|
165
|
+
console.log(` 通知模式: ${pluginConfig.notifications!.mode || "agent"}`);
|
|
166
|
+
if (pluginConfig.notifications!.agentId) {
|
|
167
|
+
console.log(` Agent ID: ${pluginConfig.notifications!.agentId}`);
|
|
168
|
+
}
|
|
161
169
|
console.log(` 静默窗口: ${pluginConfig.notifications!.quietWindowMs}ms`);
|
|
162
170
|
console.log(` 批次超时: ${pluginConfig.notifications!.batchTimeoutMs}ms`);
|
|
163
171
|
console.log(` 通知渠道: ${pluginConfig.notifications!.channel || "last"}`);
|
|
@@ -216,6 +224,8 @@ export async function runSetup(): Promise<void> {
|
|
|
216
224
|
indexExistingOnStart: true,
|
|
217
225
|
notifications: {
|
|
218
226
|
enabled: existing.notifications?.enabled ?? false,
|
|
227
|
+
mode: existing.notifications?.mode ?? "agent",
|
|
228
|
+
agentId: existing.notifications?.agentId,
|
|
219
229
|
quietWindowMs: existing.notifications?.quietWindowMs ?? 30000,
|
|
220
230
|
batchTimeoutMs: existing.notifications?.batchTimeoutMs ?? 600000,
|
|
221
231
|
channel: existing.notifications?.channel ?? "last",
|
package/src/types.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type MediaSearchResult = {
|
|
|
35
35
|
*/
|
|
36
36
|
export type NotificationConfig = {
|
|
37
37
|
enabled: boolean; // 是否启用通知,默认 false
|
|
38
|
+
mode?: "agent" | "message-send" | "auto"; // 通知模式:默认 agent(唤醒 agent 回复)
|
|
39
|
+
agentId?: string; // mode=agent/auto 时用于触发的 agent,默认 main(或配置中的默认 agent)
|
|
38
40
|
quietWindowMs: number; // 静默窗口(毫秒),默认 30000
|
|
39
41
|
batchTimeoutMs: number; // 批次最大超时(毫秒),默认 600000
|
|
40
42
|
channel?: string; // hooks/agent 投递渠道,默认 "last"
|