@memtensor/memos-local-openclaw-plugin 1.0.0 → 1.0.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.
@@ -210,6 +210,10 @@ class ViewerServer {
210
210
  this.serveConfig(res);
211
211
  else if (p === "/api/config" && req.method === "PUT")
212
212
  this.handleSaveConfig(req, res);
213
+ else if (p === "/api/test-model" && req.method === "POST")
214
+ this.handleTestModel(req, res);
215
+ else if (p === "/api/fallback-model" && req.method === "GET")
216
+ this.serveFallbackModel(res);
213
217
  else if (p === "/api/auth/logout" && req.method === "POST")
214
218
  this.handleLogout(req, res);
215
219
  else if (p === "/api/migrate/scan" && req.method === "GET")
@@ -553,7 +557,8 @@ class ViewerServer {
553
557
  try {
554
558
  ftsResults = db.prepare("SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100").all(q).filter(passesFilter);
555
559
  }
556
- catch {
560
+ catch { /* FTS syntax error, fall through */ }
561
+ if (ftsResults.length === 0) {
557
562
  ftsResults = db.prepare("SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100").all(`%${q}%`, `%${q}%`).filter(passesFilter);
558
563
  }
559
564
  const SEMANTIC_THRESHOLD = 0.64;
@@ -584,16 +589,12 @@ class ViewerServer {
584
589
  }
585
590
  }
586
591
  for (const r of ftsResults) {
587
- if (seenIds.has(r.id))
588
- continue;
589
- const vscore = scoreMap.get(r.id);
590
- if (vscore !== undefined && vscore < SEMANTIC_THRESHOLD)
591
- continue;
592
- seenIds.add(r.id);
593
- merged.push(r);
594
- }
595
- const fallback = merged.length === 0 && ftsResults.length > 0;
596
- const results = fallback ? ftsResults.slice(0, 20) : merged;
592
+ if (!seenIds.has(r.id)) {
593
+ seenIds.add(r.id);
594
+ merged.push(r);
595
+ }
596
+ }
597
+ const results = merged.length > 0 ? merged : ftsResults.slice(0, 20);
597
598
  this.store.recordViewerEvent("search");
598
599
  this.jsonResponse(res, {
599
600
  results,
@@ -601,7 +602,6 @@ class ViewerServer {
601
602
  vectorCount: vectorResults.length,
602
603
  ftsCount: ftsResults.length,
603
604
  total: results.length,
604
- fallbackFts: fallback,
605
605
  });
606
606
  }
607
607
  // ─── Skills API ───
@@ -754,9 +754,10 @@ class ViewerServer {
754
754
  this.jsonResponse(res, { ok: true, skillId, visibility });
755
755
  }
756
756
  catch (err) {
757
- this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${err}`);
758
- res.writeHead(400, { "Content-Type": "application/json" });
759
- res.end(JSON.stringify({ error: String(err) }));
757
+ const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
758
+ this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
759
+ res.writeHead(500, { "Content-Type": "application/json" });
760
+ res.end(JSON.stringify({ error: errMsg }));
760
761
  }
761
762
  });
762
763
  }
@@ -962,20 +963,27 @@ class ViewerServer {
962
963
  this.jsonResponse(res, { ok: true, deleted: count });
963
964
  }
964
965
  handleDeleteAll(res) {
965
- const result = this.store.deleteAll();
966
- // Clean up skills-store directory
967
- const skillsStoreDir = node_path_1.default.join(this.dataDir, "skills-store");
968
966
  try {
969
- if (node_fs_1.default.existsSync(skillsStoreDir)) {
970
- node_fs_1.default.rmSync(skillsStoreDir, { recursive: true });
971
- node_fs_1.default.mkdirSync(skillsStoreDir, { recursive: true });
972
- this.log.info("Cleared skills-store directory");
967
+ const result = this.store.deleteAll();
968
+ const skillsStoreDir = node_path_1.default.join(this.dataDir, "skills-store");
969
+ try {
970
+ if (node_fs_1.default.existsSync(skillsStoreDir)) {
971
+ node_fs_1.default.rmSync(skillsStoreDir, { recursive: true });
972
+ node_fs_1.default.mkdirSync(skillsStoreDir, { recursive: true });
973
+ this.log.info("Cleared skills-store directory");
974
+ }
975
+ }
976
+ catch (err) {
977
+ this.log.warn(`Failed to clear skills-store: ${err}`);
973
978
  }
979
+ this.jsonResponse(res, { ok: true, deleted: result });
974
980
  }
975
981
  catch (err) {
976
- this.log.warn(`Failed to clear skills-store: ${err}`);
982
+ const msg = err instanceof Error ? err.message : String(err);
983
+ this.log.error(`handleDeleteAll error: ${msg}`);
984
+ res.writeHead(500, { "Content-Type": "application/json" });
985
+ res.end(JSON.stringify({ ok: false, error: msg }));
977
986
  }
978
- this.jsonResponse(res, { ok: true, deleted: result });
979
987
  }
980
988
  // ─── Helpers ───
981
989
  // ─── Config API ───
@@ -1058,6 +1066,157 @@ class ViewerServer {
1058
1066
  }
1059
1067
  });
1060
1068
  }
1069
+ handleTestModel(req, res) {
1070
+ this.readBody(req, async (body) => {
1071
+ try {
1072
+ const { type, provider, model, endpoint, apiKey } = JSON.parse(body);
1073
+ if (!provider) {
1074
+ this.jsonResponse(res, { ok: false, error: "provider is required" });
1075
+ return;
1076
+ }
1077
+ if (type === "embedding") {
1078
+ await this.testEmbeddingModel(provider, model, endpoint, apiKey);
1079
+ this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
1080
+ }
1081
+ else {
1082
+ await this.testChatModel(provider, model, endpoint, apiKey);
1083
+ this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
1084
+ }
1085
+ }
1086
+ catch (e) {
1087
+ const msg = e instanceof Error ? e.message : String(e);
1088
+ this.log.warn(`test-model failed: ${msg}`);
1089
+ this.jsonResponse(res, { ok: false, error: msg });
1090
+ }
1091
+ });
1092
+ }
1093
+ serveFallbackModel(res) {
1094
+ try {
1095
+ const cfgPath = this.getOpenClawConfigPath();
1096
+ if (!node_fs_1.default.existsSync(cfgPath)) {
1097
+ this.jsonResponse(res, { available: false });
1098
+ return;
1099
+ }
1100
+ const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
1101
+ const agentModel = raw?.agents?.defaults?.model?.primary;
1102
+ if (!agentModel) {
1103
+ this.jsonResponse(res, { available: false });
1104
+ return;
1105
+ }
1106
+ const [providerKey, modelId] = agentModel.includes("/")
1107
+ ? agentModel.split("/", 2)
1108
+ : [undefined, agentModel];
1109
+ const providerCfg = providerKey
1110
+ ? raw?.models?.providers?.[providerKey]
1111
+ : Object.values(raw?.models?.providers ?? {})[0];
1112
+ if (!providerCfg || !providerCfg.baseUrl || !providerCfg.apiKey) {
1113
+ this.jsonResponse(res, { available: false });
1114
+ return;
1115
+ }
1116
+ this.jsonResponse(res, { available: true, model: modelId || agentModel, baseUrl: providerCfg.baseUrl });
1117
+ }
1118
+ catch {
1119
+ this.jsonResponse(res, { available: false });
1120
+ }
1121
+ }
1122
+ async testEmbeddingModel(provider, model, endpoint, apiKey) {
1123
+ if (provider === "local") {
1124
+ return;
1125
+ }
1126
+ const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
1127
+ const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
1128
+ const headers = {
1129
+ "Content-Type": "application/json",
1130
+ "Authorization": `Bearer ${apiKey}`,
1131
+ };
1132
+ if (provider === "cohere") {
1133
+ headers["Authorization"] = `Bearer ${apiKey}`;
1134
+ const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
1135
+ method: "POST",
1136
+ headers,
1137
+ body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
1138
+ signal: AbortSignal.timeout(15_000),
1139
+ });
1140
+ if (!resp.ok) {
1141
+ const txt = await resp.text();
1142
+ throw new Error(`Cohere embed ${resp.status}: ${txt}`);
1143
+ }
1144
+ return;
1145
+ }
1146
+ if (provider === "gemini") {
1147
+ const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
1148
+ const resp = await fetch(url, {
1149
+ method: "POST",
1150
+ headers: { "Content-Type": "application/json" },
1151
+ body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
1152
+ signal: AbortSignal.timeout(15_000),
1153
+ });
1154
+ if (!resp.ok) {
1155
+ const txt = await resp.text();
1156
+ throw new Error(`Gemini embed ${resp.status}: ${txt}`);
1157
+ }
1158
+ return;
1159
+ }
1160
+ const resp = await fetch(embUrl, {
1161
+ method: "POST",
1162
+ headers,
1163
+ body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
1164
+ signal: AbortSignal.timeout(15_000),
1165
+ });
1166
+ if (!resp.ok) {
1167
+ const txt = await resp.text();
1168
+ throw new Error(`${resp.status}: ${txt}`);
1169
+ }
1170
+ }
1171
+ async testChatModel(provider, model, endpoint, apiKey) {
1172
+ const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
1173
+ if (provider === "anthropic") {
1174
+ const url = endpoint || "https://api.anthropic.com/v1/messages";
1175
+ const resp = await fetch(url, {
1176
+ method: "POST",
1177
+ headers: {
1178
+ "Content-Type": "application/json",
1179
+ "x-api-key": apiKey,
1180
+ "anthropic-version": "2023-06-01",
1181
+ },
1182
+ body: JSON.stringify({ model: model || "claude-3-haiku-20240307", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
1183
+ signal: AbortSignal.timeout(15_000),
1184
+ });
1185
+ if (!resp.ok) {
1186
+ const txt = await resp.text();
1187
+ throw new Error(`Anthropic ${resp.status}: ${txt}`);
1188
+ }
1189
+ return;
1190
+ }
1191
+ if (provider === "gemini") {
1192
+ const url = `https://generativelanguage.googleapis.com/v1/models/${model || "gemini-1.5-flash"}:generateContent?key=${apiKey}`;
1193
+ const resp = await fetch(url, {
1194
+ method: "POST",
1195
+ headers: { "Content-Type": "application/json" },
1196
+ body: JSON.stringify({ contents: [{ parts: [{ text: "hi" }] }], generationConfig: { maxOutputTokens: 5 } }),
1197
+ signal: AbortSignal.timeout(15_000),
1198
+ });
1199
+ if (!resp.ok) {
1200
+ const txt = await resp.text();
1201
+ throw new Error(`Gemini ${resp.status}: ${txt}`);
1202
+ }
1203
+ return;
1204
+ }
1205
+ const chatUrl = baseUrl.endsWith("/chat/completions") ? baseUrl : `${baseUrl}/chat/completions`;
1206
+ const resp = await fetch(chatUrl, {
1207
+ method: "POST",
1208
+ headers: {
1209
+ "Content-Type": "application/json",
1210
+ "Authorization": `Bearer ${apiKey}`,
1211
+ },
1212
+ body: JSON.stringify({ model: model || "gpt-4o-mini", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
1213
+ signal: AbortSignal.timeout(15_000),
1214
+ });
1215
+ if (!resp.ok) {
1216
+ const txt = await resp.text();
1217
+ throw new Error(`${resp.status}: ${txt}`);
1218
+ }
1219
+ }
1061
1220
  serveLogs(res, url) {
1062
1221
  const limit = Math.min(Number(url.searchParams.get("limit") ?? 20), 200);
1063
1222
  const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0));