@memtensor/memos-local-openclaw-plugin 1.0.0 → 1.0.2-beta.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 +8 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +20 -9
- 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 +223 -70
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +6 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +236 -24
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +41 -0
- package/package.json +1 -1
- package/scripts/postinstall.cjs +72 -0
- package/src/storage/sqlite.ts +19 -9
- package/src/viewer/html.ts +223 -70
- package/src/viewer/server.ts +228 -20
package/src/viewer/server.ts
CHANGED
|
@@ -215,6 +215,9 @@ export class ViewerServer {
|
|
|
215
215
|
else if (p === "/api/log-tools" && req.method === "GET") this.serveLogTools(res);
|
|
216
216
|
else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
|
|
217
217
|
else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
|
|
218
|
+
else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
|
|
219
|
+
else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
|
|
220
|
+
else if (p === "/api/update-check" && req.method === "GET") this.handleUpdateCheck(res);
|
|
218
221
|
else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
|
|
219
222
|
else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
|
|
220
223
|
else if (p === "/api/migrate/start" && req.method === "POST") this.handleMigrateStart(req, res);
|
|
@@ -545,7 +548,8 @@ export class ViewerServer {
|
|
|
545
548
|
ftsResults = db.prepare(
|
|
546
549
|
"SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100",
|
|
547
550
|
).all(q).filter(passesFilter);
|
|
548
|
-
} catch {
|
|
551
|
+
} catch { /* FTS syntax error, fall through */ }
|
|
552
|
+
if (ftsResults.length === 0) {
|
|
549
553
|
ftsResults = db.prepare(
|
|
550
554
|
"SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100",
|
|
551
555
|
).all(`%${q}%`, `%${q}%`).filter(passesFilter);
|
|
@@ -576,14 +580,10 @@ export class ViewerServer {
|
|
|
576
580
|
if (!seenIds.has(r.id)) { seenIds.add(r.id); merged.push(r); }
|
|
577
581
|
}
|
|
578
582
|
for (const r of ftsResults) {
|
|
579
|
-
if (seenIds.has(r.id))
|
|
580
|
-
const vscore = scoreMap.get(r.id);
|
|
581
|
-
if (vscore !== undefined && vscore < SEMANTIC_THRESHOLD) continue;
|
|
582
|
-
seenIds.add(r.id); merged.push(r);
|
|
583
|
+
if (!seenIds.has(r.id)) { seenIds.add(r.id); merged.push(r); }
|
|
583
584
|
}
|
|
584
585
|
|
|
585
|
-
const
|
|
586
|
-
const results = fallback ? ftsResults.slice(0, 20) : merged;
|
|
586
|
+
const results = merged.length > 0 ? merged : ftsResults.slice(0, 20);
|
|
587
587
|
|
|
588
588
|
this.store.recordViewerEvent("search");
|
|
589
589
|
this.jsonResponse(res, {
|
|
@@ -592,7 +592,6 @@ export class ViewerServer {
|
|
|
592
592
|
vectorCount: vectorResults.length,
|
|
593
593
|
ftsCount: ftsResults.length,
|
|
594
594
|
total: results.length,
|
|
595
|
-
fallbackFts: fallback,
|
|
596
595
|
});
|
|
597
596
|
}
|
|
598
597
|
|
|
@@ -751,9 +750,10 @@ export class ViewerServer {
|
|
|
751
750
|
this.store.setSkillVisibility(skillId, visibility);
|
|
752
751
|
this.jsonResponse(res, { ok: true, skillId, visibility });
|
|
753
752
|
} catch (err) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
res.
|
|
753
|
+
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
754
|
+
this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
|
|
755
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
756
|
+
res.end(JSON.stringify({ error: errMsg }));
|
|
757
757
|
}
|
|
758
758
|
});
|
|
759
759
|
}
|
|
@@ -930,19 +930,25 @@ export class ViewerServer {
|
|
|
930
930
|
}
|
|
931
931
|
|
|
932
932
|
private handleDeleteAll(res: http.ServerResponse): void {
|
|
933
|
-
const result = this.store.deleteAll();
|
|
934
|
-
// Clean up skills-store directory
|
|
935
|
-
const skillsStoreDir = path.join(this.dataDir, "skills-store");
|
|
936
933
|
try {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
934
|
+
const result = this.store.deleteAll();
|
|
935
|
+
const skillsStoreDir = path.join(this.dataDir, "skills-store");
|
|
936
|
+
try {
|
|
937
|
+
if (fs.existsSync(skillsStoreDir)) {
|
|
938
|
+
fs.rmSync(skillsStoreDir, { recursive: true });
|
|
939
|
+
fs.mkdirSync(skillsStoreDir, { recursive: true });
|
|
940
|
+
this.log.info("Cleared skills-store directory");
|
|
941
|
+
}
|
|
942
|
+
} catch (err) {
|
|
943
|
+
this.log.warn(`Failed to clear skills-store: ${err}`);
|
|
941
944
|
}
|
|
945
|
+
this.jsonResponse(res, { ok: true, deleted: result });
|
|
942
946
|
} catch (err) {
|
|
943
|
-
|
|
947
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
948
|
+
this.log.error(`handleDeleteAll error: ${msg}`);
|
|
949
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
950
|
+
res.end(JSON.stringify({ ok: false, error: msg }));
|
|
944
951
|
}
|
|
945
|
-
this.jsonResponse(res, { ok: true, deleted: result });
|
|
946
952
|
}
|
|
947
953
|
|
|
948
954
|
// ─── Helpers ───
|
|
@@ -1023,6 +1029,208 @@ export class ViewerServer {
|
|
|
1023
1029
|
});
|
|
1024
1030
|
}
|
|
1025
1031
|
|
|
1032
|
+
private handleTestModel(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1033
|
+
this.readBody(req, async (body) => {
|
|
1034
|
+
try {
|
|
1035
|
+
const { type, provider, model, endpoint, apiKey } = JSON.parse(body);
|
|
1036
|
+
if (!provider) {
|
|
1037
|
+
this.jsonResponse(res, { ok: false, error: "provider is required" });
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (type === "embedding") {
|
|
1041
|
+
await this.testEmbeddingModel(provider, model, endpoint, apiKey);
|
|
1042
|
+
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
|
|
1043
|
+
} else {
|
|
1044
|
+
await this.testChatModel(provider, model, endpoint, apiKey);
|
|
1045
|
+
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
|
|
1046
|
+
}
|
|
1047
|
+
} catch (e: unknown) {
|
|
1048
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1049
|
+
this.log.warn(`test-model failed: ${msg}`);
|
|
1050
|
+
this.jsonResponse(res, { ok: false, error: msg });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private serveFallbackModel(res: http.ServerResponse): void {
|
|
1056
|
+
try {
|
|
1057
|
+
const cfgPath = this.getOpenClawConfigPath();
|
|
1058
|
+
if (!fs.existsSync(cfgPath)) {
|
|
1059
|
+
this.jsonResponse(res, { available: false });
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
1063
|
+
const agentModel: string | undefined = raw?.agents?.defaults?.model?.primary;
|
|
1064
|
+
if (!agentModel) {
|
|
1065
|
+
this.jsonResponse(res, { available: false });
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const [providerKey, modelId] = agentModel.includes("/")
|
|
1069
|
+
? agentModel.split("/", 2)
|
|
1070
|
+
: [undefined, agentModel];
|
|
1071
|
+
const providerCfg = providerKey
|
|
1072
|
+
? raw?.models?.providers?.[providerKey]
|
|
1073
|
+
: Object.values(raw?.models?.providers ?? {})[0] as Record<string, unknown> | undefined;
|
|
1074
|
+
if (!providerCfg || !providerCfg.baseUrl || !providerCfg.apiKey) {
|
|
1075
|
+
this.jsonResponse(res, { available: false });
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
this.jsonResponse(res, { available: true, model: modelId || agentModel, baseUrl: providerCfg.baseUrl });
|
|
1079
|
+
} catch {
|
|
1080
|
+
this.jsonResponse(res, { available: false });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private findPluginPackageJson(): string | null {
|
|
1085
|
+
let dir = __dirname;
|
|
1086
|
+
for (let i = 0; i < 6; i++) {
|
|
1087
|
+
const candidate = path.join(dir, "package.json");
|
|
1088
|
+
if (fs.existsSync(candidate)) {
|
|
1089
|
+
try {
|
|
1090
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
|
|
1091
|
+
if (pkg.name && pkg.name.includes("memos-local")) return candidate;
|
|
1092
|
+
} catch { /* skip */ }
|
|
1093
|
+
}
|
|
1094
|
+
dir = path.dirname(dir);
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private async handleUpdateCheck(res: http.ServerResponse): Promise<void> {
|
|
1100
|
+
try {
|
|
1101
|
+
const pkgPath = this.findPluginPackageJson();
|
|
1102
|
+
if (!pkgPath) {
|
|
1103
|
+
this.jsonResponse(res, { updateAvailable: false, error: "package.json not found" });
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
1107
|
+
const current = pkg.version as string;
|
|
1108
|
+
const name = pkg.name as string;
|
|
1109
|
+
if (!current || !name) {
|
|
1110
|
+
this.jsonResponse(res, { updateAvailable: false, current });
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const npmResp = await fetch(`https://registry.npmjs.org/${name}/latest`, {
|
|
1114
|
+
signal: AbortSignal.timeout(6_000),
|
|
1115
|
+
});
|
|
1116
|
+
if (!npmResp.ok) {
|
|
1117
|
+
this.jsonResponse(res, { updateAvailable: false, current });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const data = await npmResp.json() as { version?: string };
|
|
1121
|
+
const latest = data.version ?? current;
|
|
1122
|
+
this.jsonResponse(res, {
|
|
1123
|
+
updateAvailable: latest !== current,
|
|
1124
|
+
current,
|
|
1125
|
+
latest,
|
|
1126
|
+
packageName: name,
|
|
1127
|
+
});
|
|
1128
|
+
} catch (e) {
|
|
1129
|
+
this.log.warn(`handleUpdateCheck error: ${e}`);
|
|
1130
|
+
this.jsonResponse(res, { updateAvailable: false, error: String(e) });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
|
|
1135
|
+
if (provider === "local") {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1139
|
+
const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
|
|
1140
|
+
const headers: Record<string, string> = {
|
|
1141
|
+
"Content-Type": "application/json",
|
|
1142
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1143
|
+
};
|
|
1144
|
+
if (provider === "cohere") {
|
|
1145
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1146
|
+
const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
|
|
1147
|
+
method: "POST",
|
|
1148
|
+
headers,
|
|
1149
|
+
body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
|
|
1150
|
+
signal: AbortSignal.timeout(15_000),
|
|
1151
|
+
});
|
|
1152
|
+
if (!resp.ok) {
|
|
1153
|
+
const txt = await resp.text();
|
|
1154
|
+
throw new Error(`Cohere embed ${resp.status}: ${txt}`);
|
|
1155
|
+
}
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (provider === "gemini") {
|
|
1159
|
+
const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
|
|
1160
|
+
const resp = await fetch(url, {
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
headers: { "Content-Type": "application/json" },
|
|
1163
|
+
body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
|
|
1164
|
+
signal: AbortSignal.timeout(15_000),
|
|
1165
|
+
});
|
|
1166
|
+
if (!resp.ok) {
|
|
1167
|
+
const txt = await resp.text();
|
|
1168
|
+
throw new Error(`Gemini embed ${resp.status}: ${txt}`);
|
|
1169
|
+
}
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const resp = await fetch(embUrl, {
|
|
1173
|
+
method: "POST",
|
|
1174
|
+
headers,
|
|
1175
|
+
body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
|
|
1176
|
+
signal: AbortSignal.timeout(15_000),
|
|
1177
|
+
});
|
|
1178
|
+
if (!resp.ok) {
|
|
1179
|
+
const txt = await resp.text();
|
|
1180
|
+
throw new Error(`${resp.status}: ${txt}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
private async testChatModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
|
|
1185
|
+
const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1186
|
+
if (provider === "anthropic") {
|
|
1187
|
+
const url = endpoint || "https://api.anthropic.com/v1/messages";
|
|
1188
|
+
const resp = await fetch(url, {
|
|
1189
|
+
method: "POST",
|
|
1190
|
+
headers: {
|
|
1191
|
+
"Content-Type": "application/json",
|
|
1192
|
+
"x-api-key": apiKey,
|
|
1193
|
+
"anthropic-version": "2023-06-01",
|
|
1194
|
+
},
|
|
1195
|
+
body: JSON.stringify({ model: model || "claude-3-haiku-20240307", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
|
|
1196
|
+
signal: AbortSignal.timeout(15_000),
|
|
1197
|
+
});
|
|
1198
|
+
if (!resp.ok) {
|
|
1199
|
+
const txt = await resp.text();
|
|
1200
|
+
throw new Error(`Anthropic ${resp.status}: ${txt}`);
|
|
1201
|
+
}
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (provider === "gemini") {
|
|
1205
|
+
const url = `https://generativelanguage.googleapis.com/v1/models/${model || "gemini-1.5-flash"}:generateContent?key=${apiKey}`;
|
|
1206
|
+
const resp = await fetch(url, {
|
|
1207
|
+
method: "POST",
|
|
1208
|
+
headers: { "Content-Type": "application/json" },
|
|
1209
|
+
body: JSON.stringify({ contents: [{ parts: [{ text: "hi" }] }], generationConfig: { maxOutputTokens: 5 } }),
|
|
1210
|
+
signal: AbortSignal.timeout(15_000),
|
|
1211
|
+
});
|
|
1212
|
+
if (!resp.ok) {
|
|
1213
|
+
const txt = await resp.text();
|
|
1214
|
+
throw new Error(`Gemini ${resp.status}: ${txt}`);
|
|
1215
|
+
}
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const chatUrl = baseUrl.endsWith("/chat/completions") ? baseUrl : `${baseUrl}/chat/completions`;
|
|
1219
|
+
const resp = await fetch(chatUrl, {
|
|
1220
|
+
method: "POST",
|
|
1221
|
+
headers: {
|
|
1222
|
+
"Content-Type": "application/json",
|
|
1223
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1224
|
+
},
|
|
1225
|
+
body: JSON.stringify({ model: model || "gpt-4o-mini", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
|
|
1226
|
+
signal: AbortSignal.timeout(15_000),
|
|
1227
|
+
});
|
|
1228
|
+
if (!resp.ok) {
|
|
1229
|
+
const txt = await resp.text();
|
|
1230
|
+
throw new Error(`${resp.status}: ${txt}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1026
1234
|
private serveLogs(res: http.ServerResponse, url: URL): void {
|
|
1027
1235
|
const limit = Math.min(Number(url.searchParams.get("limit") ?? 20), 200);
|
|
1028
1236
|
const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0));
|