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