@memtensor/memos-local-openclaw-plugin 1.0.7 → 1.0.8-beta.2
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/.env.example +4 -0
- package/index.ts +74 -49
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
- package/scripts/postinstall.cjs +59 -25
- package/skill/memos-memory-guide/SKILL.md +5 -2
- package/src/ingest/providers/index.ts +14 -1
- package/src/recall/engine.ts +1 -1
- package/src/shared/llm-call.ts +14 -1
- package/src/storage/sqlite.ts +150 -6
- package/src/viewer/html.ts +779 -220
- package/src/viewer/server.ts +189 -8
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/telemetry.credentials.json +0 -5
package/src/viewer/server.ts
CHANGED
|
@@ -315,6 +315,7 @@ export class ViewerServer {
|
|
|
315
315
|
else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
|
|
316
316
|
else if (p === "/api/search") this.serveSearch(req, res, url);
|
|
317
317
|
else if (p === "/api/tasks" && req.method === "GET") this.serveTasks(res, url);
|
|
318
|
+
else if (p === "/api/task-search" && req.method === "GET") this.serveTaskSearch(res, url);
|
|
318
319
|
else if (p.match(/^\/api\/task\/[^/]+\/retry-skill$/) && req.method === "POST") this.handleTaskRetrySkill(req, res, p);
|
|
319
320
|
else if (p.startsWith("/api/task/") && req.method === "DELETE") this.handleTaskDelete(res, p);
|
|
320
321
|
else if (p.startsWith("/api/task/") && req.method === "PUT") this.handleTaskUpdate(req, res, p);
|
|
@@ -323,6 +324,8 @@ export class ViewerServer {
|
|
|
323
324
|
else if (p.match(/^\/api\/skill\/[^/]+\/download$/) && req.method === "GET") this.serveSkillDownload(res, p);
|
|
324
325
|
else if (p.match(/^\/api\/skill\/[^/]+\/files$/) && req.method === "GET") this.serveSkillFiles(res, p);
|
|
325
326
|
else if (p.match(/^\/api\/skill\/[^/]+\/visibility$/) && req.method === "PUT") this.handleSkillVisibility(req, res, p);
|
|
327
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/disable$/) && req.method === "PUT") this.handleSkillDisable(res, p);
|
|
328
|
+
else if (p.match(/^\/api\/skill\/[^/]+\/enable$/) && req.method === "PUT") this.handleSkillEnable(res, p);
|
|
326
329
|
else if (p.startsWith("/api/skill/") && req.method === "DELETE") this.handleSkillDelete(res, p);
|
|
327
330
|
else if (p.startsWith("/api/skill/") && req.method === "PUT") this.handleSkillUpdate(req, res, p);
|
|
328
331
|
else if (p.startsWith("/api/skill/") && req.method === "GET") this.serveSkillDetail(res, p);
|
|
@@ -608,9 +611,10 @@ export class ViewerServer {
|
|
|
608
611
|
this.store.recordViewerEvent("tasks_list");
|
|
609
612
|
const status = url.searchParams.get("status") ?? undefined;
|
|
610
613
|
const owner = url.searchParams.get("owner") ?? undefined;
|
|
614
|
+
const session = url.searchParams.get("session") ?? undefined;
|
|
611
615
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
612
616
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
613
|
-
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner });
|
|
617
|
+
const { tasks, total } = this.store.listTasks({ status, limit, offset, owner, session });
|
|
614
618
|
|
|
615
619
|
const db = (this.store as any).db;
|
|
616
620
|
const items = tasks.map((t) => {
|
|
@@ -631,9 +635,65 @@ export class ViewerServer {
|
|
|
631
635
|
};
|
|
632
636
|
});
|
|
633
637
|
|
|
638
|
+
this.backfillTaskEmbeddings(items);
|
|
634
639
|
this.jsonResponse(res, { tasks: items, total, limit, offset });
|
|
635
640
|
}
|
|
636
641
|
|
|
642
|
+
private async serveTaskSearch(res: http.ServerResponse, url: URL): Promise<void> {
|
|
643
|
+
const q = (url.searchParams.get("q") ?? "").trim();
|
|
644
|
+
if (!q) { this.jsonResponse(res, { tasks: [], total: 0 }); return; }
|
|
645
|
+
|
|
646
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
647
|
+
const maxResults = Math.min(50, Math.max(1, Number(url.searchParams.get("limit")) || 20));
|
|
648
|
+
|
|
649
|
+
const scoreMap = new Map<string, number>();
|
|
650
|
+
|
|
651
|
+
if (this.embedder) {
|
|
652
|
+
try {
|
|
653
|
+
const [queryVec] = await this.embedder.embed([q]);
|
|
654
|
+
const allEmb = this.store.getTaskEmbeddings(owner);
|
|
655
|
+
for (const { taskId, vector } of allEmb) {
|
|
656
|
+
let dot = 0, normA = 0, normB = 0;
|
|
657
|
+
for (let i = 0; i < queryVec.length && i < vector.length; i++) {
|
|
658
|
+
dot += queryVec[i] * vector[i];
|
|
659
|
+
normA += queryVec[i] * queryVec[i];
|
|
660
|
+
normB += vector[i] * vector[i];
|
|
661
|
+
}
|
|
662
|
+
const sim = normA > 0 && normB > 0 ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0;
|
|
663
|
+
if (sim > 0.3) scoreMap.set(taskId, sim);
|
|
664
|
+
}
|
|
665
|
+
} catch { /* embedding unavailable, fall through to FTS */ }
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const ftsResults = this.store.taskFtsSearch(q, maxResults, owner);
|
|
669
|
+
for (const { taskId, score } of ftsResults) {
|
|
670
|
+
const existing = scoreMap.get(taskId) ?? 0;
|
|
671
|
+
scoreMap.set(taskId, Math.max(existing, score * 0.8));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const sorted = [...scoreMap.entries()]
|
|
675
|
+
.sort((a, b) => b[1] - a[1])
|
|
676
|
+
.slice(0, maxResults);
|
|
677
|
+
|
|
678
|
+
const db = (this.store as any).db;
|
|
679
|
+
const tasks = sorted.map(([taskId, score]) => {
|
|
680
|
+
const t = this.store.getTask(taskId);
|
|
681
|
+
if (!t) return null;
|
|
682
|
+
const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(taskId) as { skill_status: string | null; owner: string | null } | undefined;
|
|
683
|
+
return {
|
|
684
|
+
id: t.id, sessionKey: t.sessionKey, title: t.title,
|
|
685
|
+
summary: t.summary ?? "", status: t.status,
|
|
686
|
+
startedAt: t.startedAt, endedAt: t.endedAt,
|
|
687
|
+
chunkCount: this.store.countChunksByTask(t.id),
|
|
688
|
+
skillStatus: meta?.skill_status ?? null,
|
|
689
|
+
owner: meta?.owner ?? "agent:main",
|
|
690
|
+
score,
|
|
691
|
+
};
|
|
692
|
+
}).filter(Boolean);
|
|
693
|
+
|
|
694
|
+
this.jsonResponse(res, { tasks, total: tasks.length });
|
|
695
|
+
}
|
|
696
|
+
|
|
637
697
|
private serveTaskDetail(res: http.ServerResponse, urlPath: string): void {
|
|
638
698
|
const taskId = urlPath.replace("/api/task/", "");
|
|
639
699
|
const task = this.store.getTask(taskId);
|
|
@@ -685,7 +745,7 @@ export class ViewerServer {
|
|
|
685
745
|
|
|
686
746
|
private serveStats(res: http.ServerResponse, url?: URL): void {
|
|
687
747
|
const emptyStats = {
|
|
688
|
-
totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0,
|
|
748
|
+
totalMemories: 0, totalSessions: 0, totalEmbeddings: 0, totalSkills: 0, totalTasks: 0,
|
|
689
749
|
embeddingProvider: this.embedder?.provider ?? "none",
|
|
690
750
|
dedupBreakdown: {},
|
|
691
751
|
timeRange: { earliest: null, latest: null },
|
|
@@ -728,9 +788,30 @@ export class ViewerServer {
|
|
|
728
788
|
}
|
|
729
789
|
const sessionList = db.prepare(sessionQuery).all(...sessionParams) as any[];
|
|
730
790
|
|
|
791
|
+
let taskSessionList: Array<{ session_key: string; count: number; earliest: number | null; latest: number | null }> = [];
|
|
792
|
+
try {
|
|
793
|
+
taskSessionList = db.prepare(
|
|
794
|
+
"SELECT session_key, COUNT(*) as count, MIN(started_at) as earliest, MAX(COALESCE(updated_at, started_at)) as latest FROM tasks GROUP BY session_key ORDER BY latest DESC",
|
|
795
|
+
).all() as any[];
|
|
796
|
+
} catch { /* tasks table may not exist yet */ }
|
|
797
|
+
|
|
798
|
+
let skillSessionList: Array<{ session_key: string; count: number; earliest: number | null; latest: number | null }> = [];
|
|
799
|
+
try {
|
|
800
|
+
skillSessionList = db.prepare(
|
|
801
|
+
`SELECT t.session_key as session_key, COUNT(DISTINCT ts.skill_id) as count,
|
|
802
|
+
MIN(t.started_at) as earliest, MAX(COALESCE(t.updated_at, t.started_at)) as latest
|
|
803
|
+
FROM task_skills ts JOIN tasks t ON t.id = ts.task_id
|
|
804
|
+
GROUP BY t.session_key
|
|
805
|
+
ORDER BY latest DESC`,
|
|
806
|
+
).all() as any[];
|
|
807
|
+
} catch { /* task_skills may not exist yet */ }
|
|
808
|
+
|
|
731
809
|
let skillCount = 0;
|
|
732
810
|
try { skillCount = (db.prepare("SELECT COUNT(*) as count FROM skills").get() as any).count; } catch { /* table may not exist yet */ }
|
|
733
811
|
|
|
812
|
+
let taskCount = 0;
|
|
813
|
+
try { taskCount = (db.prepare("SELECT COUNT(*) as count FROM tasks").get() as any).count; } catch { /* table may not exist yet */ }
|
|
814
|
+
|
|
734
815
|
let dedupBreakdown: Record<string, number> = {};
|
|
735
816
|
try {
|
|
736
817
|
const dedupRows = db.prepare("SELECT dedup_status, COUNT(*) as count FROM chunks GROUP BY dedup_status").all() as any[];
|
|
@@ -739,7 +820,15 @@ export class ViewerServer {
|
|
|
739
820
|
|
|
740
821
|
let owners: string[] = [];
|
|
741
822
|
try {
|
|
742
|
-
const ownerRows = db.prepare(
|
|
823
|
+
const ownerRows = db.prepare(`
|
|
824
|
+
SELECT DISTINCT owner FROM (
|
|
825
|
+
SELECT owner FROM chunks WHERE owner IS NOT NULL AND owner LIKE 'agent:%'
|
|
826
|
+
UNION
|
|
827
|
+
SELECT owner FROM tasks WHERE owner IS NOT NULL AND owner LIKE 'agent:%'
|
|
828
|
+
UNION
|
|
829
|
+
SELECT owner FROM skills WHERE owner IS NOT NULL AND owner LIKE 'agent:%'
|
|
830
|
+
) ORDER BY owner
|
|
831
|
+
`).all() as any[];
|
|
743
832
|
owners = ownerRows.map((o: any) => o.owner);
|
|
744
833
|
} catch { /* column may not exist yet */ }
|
|
745
834
|
|
|
@@ -751,11 +840,13 @@ export class ViewerServer {
|
|
|
751
840
|
|
|
752
841
|
this.jsonResponse(res, {
|
|
753
842
|
totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
|
|
754
|
-
totalSkills: skillCount,
|
|
843
|
+
totalSkills: skillCount, totalTasks: taskCount,
|
|
755
844
|
embeddingProvider: this.embedder.provider,
|
|
756
845
|
dedupBreakdown,
|
|
757
846
|
timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
|
|
758
847
|
sessions: sessionList,
|
|
848
|
+
taskSessions: taskSessionList,
|
|
849
|
+
skillSessions: skillSessionList,
|
|
759
850
|
owners,
|
|
760
851
|
currentAgentOwner,
|
|
761
852
|
});
|
|
@@ -865,7 +956,9 @@ export class ViewerServer {
|
|
|
865
956
|
private serveSkills(res: http.ServerResponse, url: URL): void {
|
|
866
957
|
const status = url.searchParams.get("status") ?? undefined;
|
|
867
958
|
const visibility = url.searchParams.get("visibility") ?? undefined;
|
|
868
|
-
|
|
959
|
+
const session = url.searchParams.get("session") ?? undefined;
|
|
960
|
+
const owner = url.searchParams.get("owner") ?? undefined;
|
|
961
|
+
let skills = this.store.listSkills({ status, session, owner });
|
|
869
962
|
if (visibility) {
|
|
870
963
|
skills = skills.filter(s => s.visibility === visibility);
|
|
871
964
|
}
|
|
@@ -1104,6 +1197,27 @@ export class ViewerServer {
|
|
|
1104
1197
|
});
|
|
1105
1198
|
}
|
|
1106
1199
|
|
|
1200
|
+
private embedTaskInBackground(taskId: string, text: string): void {
|
|
1201
|
+
if (!this.embedder || !text.trim()) return;
|
|
1202
|
+
this.embedder.embed([text]).then((vecs: number[][]) => {
|
|
1203
|
+
if (vecs.length > 0) this.store.upsertTaskEmbedding(taskId, vecs[0]);
|
|
1204
|
+
}).catch(() => {});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
private backfillTaskEmbeddings(tasks: Array<{ id: string; summary: string; title: string }>): void {
|
|
1208
|
+
if (!this.embedder) return;
|
|
1209
|
+
const db = (this.store as any).db;
|
|
1210
|
+
for (const t of tasks) {
|
|
1211
|
+
try {
|
|
1212
|
+
const exists = db.prepare("SELECT 1 FROM task_embeddings WHERE task_id = ?").get(t.id);
|
|
1213
|
+
if (!exists) {
|
|
1214
|
+
const text = `${t.title ?? ""}: ${t.summary ?? ""}`.trim();
|
|
1215
|
+
if (text.length > 1) this.embedTaskInBackground(t.id, text);
|
|
1216
|
+
}
|
|
1217
|
+
} catch { /* best-effort */ }
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1107
1221
|
private handleTaskDelete(res: http.ServerResponse, urlPath: string): void {
|
|
1108
1222
|
const taskId = urlPath.replace("/api/task/", "");
|
|
1109
1223
|
const deleted = this.store.deleteTask(taskId);
|
|
@@ -1118,12 +1232,15 @@ export class ViewerServer {
|
|
|
1118
1232
|
const data = JSON.parse(body);
|
|
1119
1233
|
const task = this.store.getTask(taskId);
|
|
1120
1234
|
if (!task) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Task not found" })); return; }
|
|
1235
|
+
const newTitle = data.title ?? task.title;
|
|
1236
|
+
const newSummary = data.summary ?? task.summary;
|
|
1121
1237
|
this.store.updateTask(taskId, {
|
|
1122
|
-
title:
|
|
1123
|
-
summary:
|
|
1238
|
+
title: newTitle,
|
|
1239
|
+
summary: newSummary,
|
|
1124
1240
|
status: data.status ?? task.status,
|
|
1125
1241
|
endedAt: task.endedAt ?? undefined,
|
|
1126
1242
|
});
|
|
1243
|
+
this.embedTaskInBackground(taskId, `${newTitle ?? ""}: ${newSummary ?? ""}`);
|
|
1127
1244
|
this.jsonResponse(res, { ok: true, taskId });
|
|
1128
1245
|
} catch (err) {
|
|
1129
1246
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -1180,6 +1297,58 @@ export class ViewerServer {
|
|
|
1180
1297
|
});
|
|
1181
1298
|
}
|
|
1182
1299
|
|
|
1300
|
+
private async handleSkillDisable(res: http.ServerResponse, urlPath: string): Promise<void> {
|
|
1301
|
+
const skillId = urlPath.split("/")[3];
|
|
1302
|
+
const skill = this.store.getSkill(skillId);
|
|
1303
|
+
if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
|
|
1304
|
+
if (skill.status === "archived") { this.jsonResponse(res, { ok: true, skillId, message: "already disabled" }); return; }
|
|
1305
|
+
|
|
1306
|
+
try {
|
|
1307
|
+
if (skill.visibility === "public") {
|
|
1308
|
+
this.store.setSkillVisibility(skillId, "private");
|
|
1309
|
+
}
|
|
1310
|
+
const hub = this.resolveHubConnection();
|
|
1311
|
+
if (hub) {
|
|
1312
|
+
await hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/skills/unpublish", {
|
|
1313
|
+
method: "POST",
|
|
1314
|
+
body: JSON.stringify({ sourceSkillId: skillId }),
|
|
1315
|
+
}).catch(() => {});
|
|
1316
|
+
}
|
|
1317
|
+
} catch (_) {}
|
|
1318
|
+
|
|
1319
|
+
try {
|
|
1320
|
+
const workspaceSkillsDir = path.join(this.dataDir, "workspace", "skills");
|
|
1321
|
+
const installedDir = path.join(workspaceSkillsDir, skill.name);
|
|
1322
|
+
if (fs.existsSync(installedDir)) {
|
|
1323
|
+
fs.rmSync(installedDir, { recursive: true, force: true });
|
|
1324
|
+
}
|
|
1325
|
+
} catch (_) {}
|
|
1326
|
+
|
|
1327
|
+
this.store.disableSkill(skillId);
|
|
1328
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
private handleSkillEnable(res: http.ServerResponse, urlPath: string): void {
|
|
1332
|
+
const skillId = urlPath.split("/")[3];
|
|
1333
|
+
const skill = this.store.getSkill(skillId);
|
|
1334
|
+
if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
|
|
1335
|
+
if (skill.status !== "archived") { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Only disabled (archived) skills can be enabled" })); return; }
|
|
1336
|
+
|
|
1337
|
+
this.store.enableSkill(skillId);
|
|
1338
|
+
|
|
1339
|
+
if (this.embedder) {
|
|
1340
|
+
const sv = this.store.getLatestSkillVersion(skillId);
|
|
1341
|
+
if (sv) {
|
|
1342
|
+
const text = `${skill.name}: ${skill.description}`;
|
|
1343
|
+
this.embedder.embed([text]).then((vecs: number[][]) => {
|
|
1344
|
+
if (vecs.length > 0) this.store.upsertSkillEmbedding(skillId, vecs[0]);
|
|
1345
|
+
}).catch(() => {});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1183
1352
|
// ─── CRUD ───
|
|
1184
1353
|
|
|
1185
1354
|
private serveMemoryDetail(res: http.ServerResponse, urlPath: string): void {
|
|
@@ -3258,7 +3427,8 @@ export class ViewerServer {
|
|
|
3258
3427
|
const providerCfg = providerKey
|
|
3259
3428
|
? raw?.models?.providers?.[providerKey]
|
|
3260
3429
|
: Object.values(raw?.models?.providers ?? {})[0] as Record<string, unknown> | undefined;
|
|
3261
|
-
|
|
3430
|
+
const resolvedKey = ViewerServer.resolveApiKeyValue(providerCfg?.apiKey);
|
|
3431
|
+
if (!providerCfg || !providerCfg.baseUrl || !resolvedKey) {
|
|
3262
3432
|
this.jsonResponse(res, { available: false });
|
|
3263
3433
|
return;
|
|
3264
3434
|
}
|
|
@@ -3268,6 +3438,17 @@ export class ViewerServer {
|
|
|
3268
3438
|
}
|
|
3269
3439
|
}
|
|
3270
3440
|
|
|
3441
|
+
private static resolveApiKeyValue(
|
|
3442
|
+
input: unknown,
|
|
3443
|
+
): string | undefined {
|
|
3444
|
+
if (!input) return undefined;
|
|
3445
|
+
if (typeof input === "string") return input;
|
|
3446
|
+
if (typeof input === "object" && input !== null && (input as any).source === "env") {
|
|
3447
|
+
return process.env[(input as any).id];
|
|
3448
|
+
}
|
|
3449
|
+
return undefined;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3271
3452
|
private findPluginPackageJson(): string | null {
|
|
3272
3453
|
let dir = __dirname;
|
|
3273
3454
|
for (let i = 0; i < 6; i++) {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"endpoint": "https://proj-xtrace-e218d9316b328f196a3c640cc7ca84-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/rum/web/v2?workspace=default-cms-1026429231103299-cn-hangzhou&service_id=a3u72ukxmr@066657d42a13a9a9f337f",
|
|
3
|
-
"pid": "a3u72ukxmr@066657d42a13a9a9f337f",
|
|
4
|
-
"env": "prod"
|
|
5
|
-
}
|