@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.
@@ -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)) continue;
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 fallback = merged.length === 0 && ftsResults.length > 0;
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
- 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) }));
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
- if (fs.existsSync(skillsStoreDir)) {
938
- fs.rmSync(skillsStoreDir, { recursive: true });
939
- fs.mkdirSync(skillsStoreDir, { recursive: true });
940
- this.log.info("Cleared skills-store directory");
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
- this.log.warn(`Failed to clear skills-store: ${err}`);
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));