@memtensor/memos-local-openclaw-plugin 1.0.2-beta.3 → 1.0.2-beta.4
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/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +41 -1
- package/dist/capture/index.js.map +1 -1
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +20 -7
- package/dist/embedding/index.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +28 -13
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +28 -13
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +28 -13
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +19 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +98 -10
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +28 -13
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +8 -14
- package/dist/ingest/worker.js.map +1 -1
- package/dist/storage/sqlite.d.ts +14 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +42 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +113 -0
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +3 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +92 -14
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +38 -85
- package/package.json +1 -1
- package/src/capture/index.ts +56 -1
- package/src/embedding/index.ts +13 -7
- package/src/ingest/providers/anthropic.ts +28 -13
- package/src/ingest/providers/bedrock.ts +28 -13
- package/src/ingest/providers/gemini.ts +28 -13
- package/src/ingest/providers/index.ts +112 -9
- package/src/ingest/providers/openai.ts +28 -13
- package/src/ingest/worker.ts +8 -15
- package/src/storage/sqlite.ts +49 -0
- package/src/viewer/html.ts +113 -0
- package/src/viewer/server.ts +92 -16
package/src/viewer/server.ts
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import readline from "node:readline";
|
|
7
7
|
import type { SqliteStore } from "../storage/sqlite";
|
|
8
8
|
import type { Embedder } from "../embedding";
|
|
9
|
-
import { Summarizer } from "../ingest/providers";
|
|
9
|
+
import { Summarizer, modelHealth } from "../ingest/providers";
|
|
10
10
|
import { findTopSimilar } from "../ingest/dedup";
|
|
11
11
|
import { stripInboundMetadata } from "../capture";
|
|
12
12
|
import { vectorSearch } from "../storage/vector";
|
|
@@ -17,6 +17,11 @@ import type { Logger, Chunk, PluginContext } from "../types";
|
|
|
17
17
|
import { viewerHTML } from "./html";
|
|
18
18
|
import { v4 as uuid } from "uuid";
|
|
19
19
|
|
|
20
|
+
function normalizeTimestamp(ts: number): number {
|
|
21
|
+
if (ts < 1e12) return ts * 1000;
|
|
22
|
+
return ts;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export interface ViewerServerOptions {
|
|
21
26
|
store: SqliteStore;
|
|
22
27
|
embedder: Embedder;
|
|
@@ -93,11 +98,28 @@ export class ViewerServer {
|
|
|
93
98
|
this.server.listen(this.port, "127.0.0.1", () => {
|
|
94
99
|
const addr = this.server!.address();
|
|
95
100
|
const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
|
|
101
|
+
this.autoCleanupPolluted();
|
|
96
102
|
resolve(`http://127.0.0.1:${actualPort}`);
|
|
97
103
|
});
|
|
98
104
|
});
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
private autoCleanupPolluted(): void {
|
|
108
|
+
try {
|
|
109
|
+
const polluted = this.store.findPollutedUserChunks();
|
|
110
|
+
let deleted = 0;
|
|
111
|
+
for (const { id } of polluted) {
|
|
112
|
+
if (this.store.deleteChunk(id)) deleted++;
|
|
113
|
+
}
|
|
114
|
+
const fixed = this.store.fixMixedUserChunks();
|
|
115
|
+
if (deleted > 0 || fixed > 0) {
|
|
116
|
+
this.log.info(`Auto-cleanup: removed ${deleted} polluted chunks, fixed ${fixed} mixed user+assistant chunks`);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
this.log.warn(`Auto-cleanup failed: ${err}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
101
123
|
stop(): void {
|
|
102
124
|
this.server?.close();
|
|
103
125
|
this.server = null;
|
|
@@ -216,9 +238,11 @@ export class ViewerServer {
|
|
|
216
238
|
else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
|
|
217
239
|
else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
|
|
218
240
|
else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
|
|
241
|
+
else if (p === "/api/model-health" && req.method === "GET") this.serveModelHealth(res);
|
|
219
242
|
else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
|
|
220
243
|
else if (p === "/api/update-check" && req.method === "GET") this.handleUpdateCheck(res);
|
|
221
244
|
else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
|
|
245
|
+
else if (p === "/api/cleanup-polluted" && req.method === "POST") this.handleCleanupPolluted(res);
|
|
222
246
|
else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
|
|
223
247
|
else if (p === "/api/migrate/start" && req.method === "POST") this.handleMigrateStart(req, res);
|
|
224
248
|
else if (p === "/api/migrate/status" && req.method === "GET") this.handleMigrateStatus(res);
|
|
@@ -485,7 +509,15 @@ export class ViewerServer {
|
|
|
485
509
|
const total = db.prepare("SELECT COUNT(*) as count FROM chunks").get() as any;
|
|
486
510
|
const sessions = db.prepare("SELECT COUNT(DISTINCT session_key) as count FROM chunks").get() as any;
|
|
487
511
|
const roles = db.prepare("SELECT role, COUNT(*) as count FROM chunks GROUP BY role").all() as any[];
|
|
488
|
-
const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks").get() as any;
|
|
512
|
+
const timeRange = db.prepare("SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM chunks WHERE dedup_status = 'active'").get() as any;
|
|
513
|
+
const MIN_VALID_TS = 1704067200000; // 2024-01-01
|
|
514
|
+
if (timeRange.earliest != null && timeRange.earliest < MIN_VALID_TS) {
|
|
515
|
+
timeRange.earliest = db.prepare("SELECT MIN(created_at) as v FROM chunks WHERE dedup_status = 'active' AND created_at >= ?").get(MIN_VALID_TS) as any;
|
|
516
|
+
timeRange.earliest = timeRange.earliest?.v ?? null;
|
|
517
|
+
}
|
|
518
|
+
if (timeRange.latest != null && timeRange.latest < MIN_VALID_TS) {
|
|
519
|
+
timeRange.latest = null;
|
|
520
|
+
}
|
|
489
521
|
let embCount = 0;
|
|
490
522
|
try { embCount = (db.prepare("SELECT COUNT(*) as count FROM embeddings").get() as any).count; } catch { /* table may not exist */ }
|
|
491
523
|
const kinds = db.prepare("SELECT kind, COUNT(*) as count FROM chunks GROUP BY kind").all() as any[];
|
|
@@ -1041,8 +1073,8 @@ export class ViewerServer {
|
|
|
1041
1073
|
return;
|
|
1042
1074
|
}
|
|
1043
1075
|
if (type === "embedding") {
|
|
1044
|
-
await this.testEmbeddingModel(provider, model, endpoint, apiKey);
|
|
1045
|
-
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}
|
|
1076
|
+
const dims = await this.testEmbeddingModel(provider, model, endpoint, apiKey);
|
|
1077
|
+
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}`, dimensions: dims });
|
|
1046
1078
|
} else {
|
|
1047
1079
|
await this.testChatModel(provider, model, endpoint, apiKey);
|
|
1048
1080
|
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
|
|
@@ -1055,6 +1087,10 @@ export class ViewerServer {
|
|
|
1055
1087
|
});
|
|
1056
1088
|
}
|
|
1057
1089
|
|
|
1090
|
+
private serveModelHealth(res: http.ServerResponse): void {
|
|
1091
|
+
this.jsonResponse(res, { models: modelHealth.getAll() });
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1058
1094
|
private serveFallbackModel(res: http.ServerResponse): void {
|
|
1059
1095
|
try {
|
|
1060
1096
|
const cfgPath = this.getOpenClawConfigPath();
|
|
@@ -1134,9 +1170,9 @@ export class ViewerServer {
|
|
|
1134
1170
|
}
|
|
1135
1171
|
}
|
|
1136
1172
|
|
|
1137
|
-
private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<
|
|
1173
|
+
private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<number | undefined> {
|
|
1138
1174
|
if (provider === "local") {
|
|
1139
|
-
return;
|
|
1175
|
+
return 384;
|
|
1140
1176
|
}
|
|
1141
1177
|
const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1142
1178
|
const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
|
|
@@ -1149,39 +1185,59 @@ export class ViewerServer {
|
|
|
1149
1185
|
const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
|
|
1150
1186
|
method: "POST",
|
|
1151
1187
|
headers,
|
|
1152
|
-
body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
|
|
1188
|
+
body: JSON.stringify({ texts: ["test embedding vector"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
|
|
1153
1189
|
signal: AbortSignal.timeout(15_000),
|
|
1154
1190
|
});
|
|
1155
1191
|
if (!resp.ok) {
|
|
1156
1192
|
const txt = await resp.text();
|
|
1157
1193
|
throw new Error(`Cohere embed ${resp.status}: ${txt}`);
|
|
1158
1194
|
}
|
|
1159
|
-
|
|
1195
|
+
const json = await resp.json() as any;
|
|
1196
|
+
const vecs = json?.embeddings?.float;
|
|
1197
|
+
if (!Array.isArray(vecs) || vecs.length === 0 || !Array.isArray(vecs[0]) || vecs[0].length === 0) {
|
|
1198
|
+
throw new Error("Cohere returned empty embedding vector");
|
|
1199
|
+
}
|
|
1200
|
+
return vecs[0].length;
|
|
1160
1201
|
}
|
|
1161
1202
|
if (provider === "gemini") {
|
|
1162
1203
|
const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
|
|
1163
1204
|
const resp = await fetch(url, {
|
|
1164
1205
|
method: "POST",
|
|
1165
1206
|
headers: { "Content-Type": "application/json" },
|
|
1166
|
-
body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
|
|
1207
|
+
body: JSON.stringify({ content: { parts: [{ text: "test embedding vector" }] } }),
|
|
1167
1208
|
signal: AbortSignal.timeout(15_000),
|
|
1168
1209
|
});
|
|
1169
1210
|
if (!resp.ok) {
|
|
1170
1211
|
const txt = await resp.text();
|
|
1171
1212
|
throw new Error(`Gemini embed ${resp.status}: ${txt}`);
|
|
1172
1213
|
}
|
|
1173
|
-
|
|
1214
|
+
const json = await resp.json() as any;
|
|
1215
|
+
const vec = json?.embedding?.values;
|
|
1216
|
+
if (!Array.isArray(vec) || vec.length === 0) {
|
|
1217
|
+
throw new Error("Gemini returned empty embedding vector");
|
|
1218
|
+
}
|
|
1219
|
+
return vec.length;
|
|
1174
1220
|
}
|
|
1175
1221
|
const resp = await fetch(embUrl, {
|
|
1176
1222
|
method: "POST",
|
|
1177
1223
|
headers,
|
|
1178
|
-
body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
|
|
1224
|
+
body: JSON.stringify({ input: ["test embedding vector"], model: model || "text-embedding-3-small" }),
|
|
1179
1225
|
signal: AbortSignal.timeout(15_000),
|
|
1180
1226
|
});
|
|
1181
1227
|
if (!resp.ok) {
|
|
1182
1228
|
const txt = await resp.text();
|
|
1183
1229
|
throw new Error(`${resp.status}: ${txt}`);
|
|
1184
1230
|
}
|
|
1231
|
+
const json = await resp.json() as any;
|
|
1232
|
+
const data = json?.data;
|
|
1233
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
1234
|
+
throw new Error("API returned no embedding data");
|
|
1235
|
+
}
|
|
1236
|
+
const vec = data[0]?.embedding;
|
|
1237
|
+
if (!Array.isArray(vec) || vec.length === 0) {
|
|
1238
|
+
throw new Error(`API returned empty embedding vector (got ${JSON.stringify(vec)?.slice(0, 100)})`);
|
|
1239
|
+
}
|
|
1240
|
+
return vec.length;
|
|
1185
1241
|
}
|
|
1186
1242
|
|
|
1187
1243
|
private async testChatModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
|
|
@@ -1256,6 +1312,28 @@ export class ViewerServer {
|
|
|
1256
1312
|
return path.join(home, ".openclaw");
|
|
1257
1313
|
}
|
|
1258
1314
|
|
|
1315
|
+
private handleCleanupPolluted(res: http.ServerResponse): void {
|
|
1316
|
+
try {
|
|
1317
|
+
const polluted = this.store.findPollutedUserChunks();
|
|
1318
|
+
let deleted = 0;
|
|
1319
|
+
for (const { id, reason } of polluted) {
|
|
1320
|
+
if (this.store.deleteChunk(id)) {
|
|
1321
|
+
deleted++;
|
|
1322
|
+
this.log.info(`Cleaned polluted chunk ${id}: ${reason}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const fixed = this.store.fixMixedUserChunks();
|
|
1326
|
+
this.log.info(`Cleanup: removed ${deleted} polluted, fixed ${fixed} mixed chunks`);
|
|
1327
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1328
|
+
res.end(JSON.stringify({ deleted, fixed, total: polluted.length }));
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1331
|
+
this.log.error(`handleCleanupPolluted error: ${msg}`);
|
|
1332
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1333
|
+
res.end(JSON.stringify({ error: msg }));
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1259
1337
|
private handleMigrateScan(res: http.ServerResponse): void {
|
|
1260
1338
|
try {
|
|
1261
1339
|
const ocHome = this.getOpenClawHome();
|
|
@@ -1499,7 +1577,6 @@ export class ViewerServer {
|
|
|
1499
1577
|
|
|
1500
1578
|
const cfgPath = this.getOpenClawConfigPath();
|
|
1501
1579
|
let summarizerCfg: any;
|
|
1502
|
-
let strongCfg: any;
|
|
1503
1580
|
try {
|
|
1504
1581
|
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
1505
1582
|
const pluginCfg = raw?.plugins?.entries?.["memos-local-openclaw-plugin"]?.config ??
|
|
@@ -1507,10 +1584,9 @@ export class ViewerServer {
|
|
|
1507
1584
|
raw?.plugins?.entries?.["memos-lite-openclaw-plugin"]?.config ??
|
|
1508
1585
|
raw?.plugins?.entries?.["memos-lite"]?.config ?? {};
|
|
1509
1586
|
summarizerCfg = pluginCfg.summarizer;
|
|
1510
|
-
strongCfg = pluginCfg.skillEvolution?.summarizer;
|
|
1511
1587
|
} catch { /* no config */ }
|
|
1512
1588
|
|
|
1513
|
-
const summarizer = new Summarizer(summarizerCfg, this.log
|
|
1589
|
+
const summarizer = new Summarizer(summarizerCfg, this.log);
|
|
1514
1590
|
|
|
1515
1591
|
// Phase 1: Import SQLite memory chunks
|
|
1516
1592
|
if (importSqlite) {
|
|
@@ -1636,8 +1712,8 @@ export class ViewerServer {
|
|
|
1636
1712
|
mergeCount: 0,
|
|
1637
1713
|
lastHitAt: null,
|
|
1638
1714
|
mergeHistory: "[]",
|
|
1639
|
-
createdAt: row.updated_at
|
|
1640
|
-
updatedAt: row.updated_at
|
|
1715
|
+
createdAt: normalizeTimestamp(row.updated_at),
|
|
1716
|
+
updatedAt: normalizeTimestamp(row.updated_at),
|
|
1641
1717
|
};
|
|
1642
1718
|
|
|
1643
1719
|
this.store.insertChunk(chunk);
|