@memtensor/memos-local-openclaw-plugin 1.0.4-beta.10 → 1.0.4-beta.11
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/dist/client/connector.d.ts +1 -0
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +37 -8
- package/dist/client/connector.js.map +1 -1
- package/dist/hub/server.d.ts +1 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +122 -28
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +9 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +26 -2
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +2 -0
- package/dist/recall/engine.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +56 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +15 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +91 -9
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +44 -23
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +35 -15
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +316 -13
- package/package.json +1 -1
- package/src/client/connector.ts +41 -8
- package/src/hub/server.ts +123 -27
- package/src/hub/user-manager.ts +42 -6
- package/src/recall/engine.ts +2 -0
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +58 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/sqlite.ts +105 -9
- package/src/types.ts +11 -0
- package/src/viewer/html.ts +44 -23
- package/src/viewer/server.ts +35 -15
package/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { IngestWorker } from "./src/ingest/worker";
|
|
|
19
19
|
import { RecallEngine } from "./src/recall/engine";
|
|
20
20
|
import { captureMessages, stripInboundMetadata } from "./src/capture";
|
|
21
21
|
import { DEFAULTS } from "./src/types";
|
|
22
|
+
import type { SearchHit } from "./src/types";
|
|
22
23
|
import { ViewerServer } from "./src/viewer/server";
|
|
23
24
|
import { HubServer } from "./src/hub/server";
|
|
24
25
|
import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
|
|
@@ -318,6 +319,21 @@ const memosLocalPlugin = {
|
|
|
318
319
|
candidates: det.candidates,
|
|
319
320
|
filtered: det.hits ?? det.filtered ?? [],
|
|
320
321
|
});
|
|
322
|
+
} else if (det && det.local && det.hub) {
|
|
323
|
+
const localHits = det.local?.hits ?? [];
|
|
324
|
+
const hubHits = (det.hub?.hits ?? []).map((h: any) => ({
|
|
325
|
+
score: h.score ?? 0,
|
|
326
|
+
role: h.source?.role ?? h.role ?? "assistant",
|
|
327
|
+
summary: h.summary ?? "",
|
|
328
|
+
original_excerpt: h.excerpt ?? h.summary ?? "",
|
|
329
|
+
origin: "hub-remote",
|
|
330
|
+
ownerName: h.ownerName ?? "",
|
|
331
|
+
groupName: h.groupName ?? "",
|
|
332
|
+
}));
|
|
333
|
+
outputText = JSON.stringify({
|
|
334
|
+
candidates: [...localHits, ...hubHits],
|
|
335
|
+
filtered: [...localHits, ...hubHits],
|
|
336
|
+
});
|
|
321
337
|
} else {
|
|
322
338
|
outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
|
|
323
339
|
}
|
|
@@ -464,6 +480,7 @@ const memosLocalPlugin = {
|
|
|
464
480
|
score: h.score,
|
|
465
481
|
summary: h.summary,
|
|
466
482
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
483
|
+
origin: h.origin || "local",
|
|
467
484
|
}));
|
|
468
485
|
|
|
469
486
|
if (result.hits.length === 0 && searchScope === "local") {
|
|
@@ -518,29 +535,79 @@ const memosLocalPlugin = {
|
|
|
518
535
|
role: h.source.role,
|
|
519
536
|
score: h.score,
|
|
520
537
|
summary: h.summary,
|
|
538
|
+
origin: h.origin || "local",
|
|
521
539
|
};
|
|
522
540
|
});
|
|
523
541
|
|
|
524
542
|
if (searchScope !== "local") {
|
|
525
543
|
const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
|
|
544
|
+
|
|
545
|
+
let filteredHubHits = hub.hits;
|
|
546
|
+
if (hub.hits.length > 0) {
|
|
547
|
+
const hubCandidates = hub.hits.map((h, i) => ({
|
|
548
|
+
index: filteredHits.length + i + 1,
|
|
549
|
+
role: (h.source?.role || "assistant") as string,
|
|
550
|
+
content: (h.summary || h.excerpt || "").slice(0, 300),
|
|
551
|
+
time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
552
|
+
}));
|
|
553
|
+
const localCandidatesForMerge = filteredHits.map((h, i) => ({
|
|
554
|
+
index: i + 1,
|
|
555
|
+
role: h.source.role,
|
|
556
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
557
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
558
|
+
}));
|
|
559
|
+
const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
|
|
560
|
+
const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
|
|
561
|
+
if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
|
|
562
|
+
const relevantSet = new Set(mergedFilter.relevant);
|
|
563
|
+
const hubStartIdx = filteredHits.length + 1;
|
|
564
|
+
filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
|
|
565
|
+
filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
|
|
566
|
+
ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const originLabel = (h: SearchHit) => {
|
|
571
|
+
if (h.origin === "hub-memory") return " [团队缓存]";
|
|
572
|
+
if (h.origin === "local-shared") return " [本机共享]";
|
|
573
|
+
return "";
|
|
574
|
+
};
|
|
526
575
|
const localText = filteredHits.length > 0
|
|
527
576
|
? filteredHits.map((h, i) => {
|
|
528
577
|
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
529
|
-
return `${i + 1}. [${h.source.role}] ${excerpt}`;
|
|
578
|
+
return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
|
|
530
579
|
}).join("\n")
|
|
531
580
|
: "(none)";
|
|
532
|
-
const hubText =
|
|
533
|
-
?
|
|
581
|
+
const hubText = filteredHubHits.length > 0
|
|
582
|
+
? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
534
583
|
: "(none)";
|
|
535
584
|
|
|
585
|
+
const localDetailsFiltered = filteredHits.map((h) => {
|
|
586
|
+
let effectiveTaskId = h.taskId;
|
|
587
|
+
if (effectiveTaskId) {
|
|
588
|
+
const t = store.getTask(effectiveTaskId);
|
|
589
|
+
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
ref: h.ref,
|
|
593
|
+
chunkId: h.ref.chunkId,
|
|
594
|
+
taskId: effectiveTaskId,
|
|
595
|
+
skillId: h.skillId,
|
|
596
|
+
role: h.source.role,
|
|
597
|
+
score: h.score,
|
|
598
|
+
summary: h.summary,
|
|
599
|
+
origin: h.origin,
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
|
|
536
603
|
return {
|
|
537
604
|
content: [{
|
|
538
605
|
type: "text",
|
|
539
606
|
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
540
607
|
}],
|
|
541
608
|
details: {
|
|
542
|
-
local: { hits:
|
|
543
|
-
hub,
|
|
609
|
+
local: { hits: localDetailsFiltered, meta: result.meta },
|
|
610
|
+
hub: { ...hub, hits: filteredHubHits },
|
|
544
611
|
},
|
|
545
612
|
};
|
|
546
613
|
}
|
|
@@ -552,9 +619,15 @@ const memosLocalPlugin = {
|
|
|
552
619
|
};
|
|
553
620
|
}
|
|
554
621
|
|
|
622
|
+
const originTag = (o?: string) => {
|
|
623
|
+
if (o === "local-shared") return " [本机共享]";
|
|
624
|
+
if (o === "hub-memory") return " [团队缓存]";
|
|
625
|
+
if (o === "hub-remote") return " [团队]";
|
|
626
|
+
return "";
|
|
627
|
+
};
|
|
555
628
|
const lines = filteredHits.map((h, i) => {
|
|
556
629
|
const excerpt = h.original_excerpt;
|
|
557
|
-
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
630
|
+
const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
|
|
558
631
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
559
632
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
560
633
|
if (h.taskId) {
|
|
@@ -609,6 +682,7 @@ const memosLocalPlugin = {
|
|
|
609
682
|
score: h.score,
|
|
610
683
|
summary: h.summary,
|
|
611
684
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
685
|
+
origin: h.origin || "local",
|
|
612
686
|
};
|
|
613
687
|
}),
|
|
614
688
|
meta: result.meta,
|
|
@@ -1058,10 +1132,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1058
1132
|
};
|
|
1059
1133
|
}
|
|
1060
1134
|
|
|
1135
|
+
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1136
|
+
let footer = "\n\n---\n";
|
|
1137
|
+
|
|
1138
|
+
if (manifest && manifest.hasCompanionFiles) {
|
|
1139
|
+
const fileSummary = manifest.files
|
|
1140
|
+
.filter(f => f.type !== "eval")
|
|
1141
|
+
.map(f => `\`${f.relativePath}\``)
|
|
1142
|
+
.join(", ");
|
|
1143
|
+
footer += `**Companion files available:** ${fileSummary}\n`;
|
|
1144
|
+
footer += `→ call \`skill_files(skillId="${resolvedSkillId}")\` to list all files\n`;
|
|
1145
|
+
footer += `→ call \`skill_file_get(skillId="${resolvedSkillId}", path="...")\` to read a specific file\n`;
|
|
1146
|
+
if (manifest.installMode === "install_recommended") {
|
|
1147
|
+
footer += `→ **Recommended:** call \`skill_install(skillId="${resolvedSkillId}")\` for persistent workspace access (many/large files)\n`;
|
|
1148
|
+
}
|
|
1149
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1150
|
+
footer += `> Already installed at: ${manifest.installedPath}/\n`;
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
footer += `To install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1061
1156
|
return {
|
|
1062
1157
|
content: [{
|
|
1063
1158
|
type: "text",
|
|
1064
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}
|
|
1159
|
+
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
1065
1160
|
}],
|
|
1066
1161
|
details: {
|
|
1067
1162
|
skillId: skill.id,
|
|
@@ -1069,6 +1164,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1069
1164
|
version: skill.version,
|
|
1070
1165
|
status: skill.status,
|
|
1071
1166
|
installed: skill.installed,
|
|
1167
|
+
companionFiles: manifest?.hasCompanionFiles ?? false,
|
|
1168
|
+
installMode: manifest?.installMode ?? "inline",
|
|
1072
1169
|
},
|
|
1073
1170
|
};
|
|
1074
1171
|
}),
|
|
@@ -1104,6 +1201,112 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1104
1201
|
{ name: "skill_install" },
|
|
1105
1202
|
);
|
|
1106
1203
|
|
|
1204
|
+
// ─── Tool: skill_files ───
|
|
1205
|
+
|
|
1206
|
+
api.registerTool(
|
|
1207
|
+
{
|
|
1208
|
+
name: "skill_files",
|
|
1209
|
+
label: "List Skill Companion Files",
|
|
1210
|
+
description:
|
|
1211
|
+
"List companion files (scripts, references, evals) for a skill. " +
|
|
1212
|
+
"Use this after skill_get to see what additional files are available. " +
|
|
1213
|
+
"Returns file names, sizes, and whether the skill recommends installation.",
|
|
1214
|
+
parameters: Type.Object({
|
|
1215
|
+
skillId: Type.String({ description: "The skill_id to inspect" }),
|
|
1216
|
+
}),
|
|
1217
|
+
execute: trackTool("skill_files", async (_toolCallId: any, params: any) => {
|
|
1218
|
+
const { skillId } = params as { skillId: string };
|
|
1219
|
+
ctx.log.debug(`skill_files called for skill=${skillId}`);
|
|
1220
|
+
|
|
1221
|
+
const manifest = skillInstaller.getCompanionManifest(skillId);
|
|
1222
|
+
if (!manifest) {
|
|
1223
|
+
return {
|
|
1224
|
+
content: [{ type: "text", text: `Skill not found: ${skillId}` }],
|
|
1225
|
+
details: { error: "not_found" },
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (!manifest.hasCompanionFiles) {
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{ type: "text", text: "This skill has no companion files (scripts, references). The SKILL.md from skill_get contains everything." }],
|
|
1232
|
+
details: manifest,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const lines: string[] = [`## Companion Files (${manifest.files.length} files, ${Math.round(manifest.totalSize / 1024)}KB total)\n`];
|
|
1237
|
+
if (manifest.scriptsCount > 0) {
|
|
1238
|
+
lines.push(`### Scripts (${manifest.scriptsCount})`);
|
|
1239
|
+
for (const f of manifest.files.filter(f => f.type === "script")) {
|
|
1240
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (manifest.referencesCount > 0) {
|
|
1244
|
+
lines.push(`\n### References (${manifest.referencesCount})`);
|
|
1245
|
+
for (const f of manifest.files.filter(f => f.type === "reference")) {
|
|
1246
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (manifest.evalsCount > 0) {
|
|
1250
|
+
lines.push(`\n### Evals (${manifest.evalsCount})`);
|
|
1251
|
+
for (const f of manifest.files.filter(f => f.type === "eval")) {
|
|
1252
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes)`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (manifest.installMode === "install_recommended") {
|
|
1257
|
+
lines.push(`\n> **Recommendation:** This skill has many/large companion files. Consider \`skill_install(skillId="${skillId}")\` for persistent workspace access.`);
|
|
1258
|
+
}
|
|
1259
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1260
|
+
lines.push(`\n> **Installed at:** ${manifest.installedPath}/`);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return {
|
|
1264
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1265
|
+
details: manifest,
|
|
1266
|
+
};
|
|
1267
|
+
}),
|
|
1268
|
+
},
|
|
1269
|
+
{ name: "skill_files" },
|
|
1270
|
+
);
|
|
1271
|
+
|
|
1272
|
+
// ─── Tool: skill_file_get ───
|
|
1273
|
+
|
|
1274
|
+
api.registerTool(
|
|
1275
|
+
{
|
|
1276
|
+
name: "skill_file_get",
|
|
1277
|
+
label: "Get Skill Companion File",
|
|
1278
|
+
description:
|
|
1279
|
+
"Read the content of a specific companion file (script, reference) from a skill. " +
|
|
1280
|
+
"Use after skill_files to retrieve a script or reference document. " +
|
|
1281
|
+
"Pass the relative path like 'scripts/deploy.sh' or 'references/api-notes.md'.",
|
|
1282
|
+
parameters: Type.Object({
|
|
1283
|
+
skillId: Type.String({ description: "The skill_id" }),
|
|
1284
|
+
path: Type.String({ description: "Relative path within the skill, e.g. 'scripts/deploy.sh'" }),
|
|
1285
|
+
}),
|
|
1286
|
+
execute: trackTool("skill_file_get", async (_toolCallId: any, params: any) => {
|
|
1287
|
+
const { skillId, path: filePath } = params as { skillId: string; path: string };
|
|
1288
|
+
ctx.log.debug(`skill_file_get called for skill=${skillId} path=${filePath}`);
|
|
1289
|
+
|
|
1290
|
+
const result = skillInstaller.readCompanionFile(skillId, filePath);
|
|
1291
|
+
if ("error" in result) {
|
|
1292
|
+
return {
|
|
1293
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
1294
|
+
details: result,
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const ext = filePath.split(".").pop() || "";
|
|
1299
|
+
const lang = { sh: "bash", py: "python", ts: "typescript", js: "javascript", json: "json", md: "markdown", yml: "yaml", yaml: "yaml" }[ext] || "";
|
|
1300
|
+
|
|
1301
|
+
return {
|
|
1302
|
+
content: [{ type: "text", text: `## ${filePath}\n\n\`\`\`${lang}\n${result.content}\n\`\`\`` }],
|
|
1303
|
+
details: { path: filePath, size: result.size },
|
|
1304
|
+
};
|
|
1305
|
+
}),
|
|
1306
|
+
},
|
|
1307
|
+
{ name: "skill_file_get" },
|
|
1308
|
+
);
|
|
1309
|
+
|
|
1107
1310
|
// ─── Tool: memory_viewer ───
|
|
1108
1311
|
|
|
1109
1312
|
const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
|
|
@@ -1614,10 +1817,40 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1614
1817
|
|
|
1615
1818
|
const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1616
1819
|
if (result.hits.length === 0) {
|
|
1617
|
-
ctx.log.debug("auto-recall: no candidates found");
|
|
1820
|
+
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1618
1821
|
const dur = performance.now() - recallT0;
|
|
1619
1822
|
store.recordToolCall("memory_search", dur, true);
|
|
1620
1823
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
|
|
1824
|
+
|
|
1825
|
+
// Even without memory hits, try skill recall
|
|
1826
|
+
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1827
|
+
if (skillAutoRecallEarly) {
|
|
1828
|
+
try {
|
|
1829
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1830
|
+
const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1831
|
+
const topSkills = skillHits.slice(0, skillLimit);
|
|
1832
|
+
if (topSkills.length > 0) {
|
|
1833
|
+
const skillLines = topSkills.map((sc, i) => {
|
|
1834
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1835
|
+
let badge = "";
|
|
1836
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1837
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1838
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1839
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
|
|
1840
|
+
});
|
|
1841
|
+
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
1842
|
+
"No direct memory matches were found, but these skills from past tasks may help:\n\n" +
|
|
1843
|
+
skillLines.join("\n\n") +
|
|
1844
|
+
"\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
|
|
1845
|
+
ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
|
|
1846
|
+
try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
|
|
1847
|
+
return { prependContext: skillContext };
|
|
1848
|
+
}
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1621
1854
|
if (query.length > 50) {
|
|
1622
1855
|
const noRecallHint =
|
|
1623
1856
|
"## Memory system — ACTION REQUIRED\n\n" +
|
|
@@ -1650,7 +1883,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1650
1883
|
const dur = performance.now() - recallT0;
|
|
1651
1884
|
store.recordToolCall("memory_search", dur, true);
|
|
1652
1885
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1653
|
-
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
|
|
1886
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
1654
1887
|
filtered: []
|
|
1655
1888
|
}), dur, true);
|
|
1656
1889
|
if (query.length > 50) {
|
|
@@ -1671,7 +1904,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1671
1904
|
|
|
1672
1905
|
const lines = filteredHits.map((h, i) => {
|
|
1673
1906
|
const excerpt = h.original_excerpt;
|
|
1674
|
-
const
|
|
1907
|
+
const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
|
|
1908
|
+
const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
|
|
1675
1909
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
1676
1910
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
1677
1911
|
if (h.taskId) {
|
|
@@ -1706,17 +1940,86 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1706
1940
|
lines.join("\n\n"),
|
|
1707
1941
|
];
|
|
1708
1942
|
if (tipsText) contextParts.push(tipsText);
|
|
1943
|
+
|
|
1944
|
+
// ─── Skill auto-recall ───
|
|
1945
|
+
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1946
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1947
|
+
let skillSection = "";
|
|
1948
|
+
|
|
1949
|
+
if (skillAutoRecall) {
|
|
1950
|
+
try {
|
|
1951
|
+
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
1952
|
+
|
|
1953
|
+
// Source 1: direct skill search based on user query
|
|
1954
|
+
try {
|
|
1955
|
+
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1956
|
+
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
1957
|
+
if (!skillCandidateMap.has(sh.skillId)) {
|
|
1958
|
+
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Source 2: skills linked to tasks from memory hits
|
|
1966
|
+
const taskIds = new Set<string>();
|
|
1967
|
+
for (const h of filteredHits) {
|
|
1968
|
+
if (h.taskId) {
|
|
1969
|
+
const t = store.getTask(h.taskId);
|
|
1970
|
+
if (t && t.status !== "skipped") taskIds.add(h.taskId);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
for (const tid of taskIds) {
|
|
1974
|
+
const linked = store.getSkillsByTask(tid);
|
|
1975
|
+
for (const rs of linked) {
|
|
1976
|
+
if (!skillCandidateMap.has(rs.skill.id)) {
|
|
1977
|
+
skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
1983
|
+
|
|
1984
|
+
if (skillCandidates.length > 0) {
|
|
1985
|
+
const skillLines = skillCandidates.map((sc, i) => {
|
|
1986
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1987
|
+
let badge = "";
|
|
1988
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1989
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1990
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1991
|
+
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
1992
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
1993
|
+
});
|
|
1994
|
+
skillSection = "\n\n## Relevant skills from past experience\n\n" +
|
|
1995
|
+
"The following skills were distilled from similar previous tasks. " +
|
|
1996
|
+
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
1997
|
+
skillLines.join("\n\n");
|
|
1998
|
+
|
|
1999
|
+
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2000
|
+
try {
|
|
2001
|
+
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2002
|
+
} catch { /* best-effort */ }
|
|
2003
|
+
} else {
|
|
2004
|
+
ctx.log.debug("auto-recall-skill: no matching skills found");
|
|
2005
|
+
}
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
ctx.log.debug(`auto-recall-skill: failed: ${err}`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (skillSection) contextParts.push(skillSection);
|
|
1709
2012
|
const context = contextParts.join("\n");
|
|
1710
2013
|
|
|
1711
2014
|
const recallDur = performance.now() - recallT0;
|
|
1712
2015
|
store.recordToolCall("memory_search", recallDur, true);
|
|
1713
2016
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1714
|
-
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
|
|
1715
|
-
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt }))
|
|
2017
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
2018
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
|
|
1716
2019
|
}), recallDur, true);
|
|
1717
2020
|
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
1718
2021
|
|
|
1719
|
-
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
|
|
2022
|
+
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
|
|
1720
2023
|
|
|
1721
2024
|
if (!sufficient) {
|
|
1722
2025
|
const searchHint =
|
package/package.json
CHANGED
package/src/client/connector.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface HubSessionInfo {
|
|
|
10
10
|
userToken: string;
|
|
11
11
|
role: UserRole;
|
|
12
12
|
connectedAt: number;
|
|
13
|
+
identityKey?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface HubStatusInfo {
|
|
@@ -54,6 +55,8 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
54
55
|
userToken: result.userToken,
|
|
55
56
|
role: "member",
|
|
56
57
|
connectedAt: Date.now(),
|
|
58
|
+
identityKey: persisted.identityKey || "",
|
|
59
|
+
lastKnownStatus: "active",
|
|
57
60
|
});
|
|
58
61
|
return store.getClientHubConnection()!;
|
|
59
62
|
}
|
|
@@ -63,6 +66,12 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
63
66
|
if (result.status === "rejected") {
|
|
64
67
|
throw new Error("Join request was rejected by the Hub admin.");
|
|
65
68
|
}
|
|
69
|
+
if (result.status === "blocked") {
|
|
70
|
+
throw new Error("Your account has been blocked by the Hub admin.");
|
|
71
|
+
}
|
|
72
|
+
if (result.status === "left" || result.status === "removed") {
|
|
73
|
+
log.info(`User status is "${result.status}", will try to rejoin.`);
|
|
74
|
+
}
|
|
66
75
|
} catch (err) {
|
|
67
76
|
if (err instanceof PendingApprovalError) throw err;
|
|
68
77
|
log.warn(`registration-status check failed, falling back to autoJoinHub: ${err}`);
|
|
@@ -78,6 +87,7 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
78
87
|
|
|
79
88
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
80
89
|
const me = await hubRequestJson(hubUrl, userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
90
|
+
const persisted = store.getClientHubConnection();
|
|
81
91
|
store.setClientHubConnection({
|
|
82
92
|
hubUrl,
|
|
83
93
|
userId: String(me.id),
|
|
@@ -85,6 +95,8 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
85
95
|
userToken,
|
|
86
96
|
role: String(me.role ?? "member") as UserRole,
|
|
87
97
|
connectedAt: Date.now(),
|
|
98
|
+
identityKey: persisted?.identityKey || String(me.identityKey ?? ""),
|
|
99
|
+
lastKnownStatus: "active",
|
|
88
100
|
});
|
|
89
101
|
return store.getClientHubConnection()!;
|
|
90
102
|
}
|
|
@@ -95,9 +107,13 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
95
107
|
const hubAddress = conn?.hubUrl || (configHubAddress ? normalizeHubUrl(configHubAddress) : "");
|
|
96
108
|
const userToken = conn?.userToken || config.sharing?.client?.userToken || "";
|
|
97
109
|
|
|
98
|
-
// If DB has a connection to a different Hub than config, the DB data is stale
|
|
99
110
|
if (conn && configHubAddress && conn.hubUrl && normalizeHubUrl(configHubAddress) !== conn.hubUrl) {
|
|
100
|
-
store.
|
|
111
|
+
store.setClientHubConnection({
|
|
112
|
+
...conn,
|
|
113
|
+
hubUrl: normalizeHubUrl(configHubAddress),
|
|
114
|
+
userToken: "",
|
|
115
|
+
lastKnownStatus: "hub_changed",
|
|
116
|
+
});
|
|
101
117
|
return { connected: false, user: null };
|
|
102
118
|
}
|
|
103
119
|
|
|
@@ -129,6 +145,8 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
129
145
|
userToken: result.userToken,
|
|
130
146
|
role: "member",
|
|
131
147
|
connectedAt: Date.now(),
|
|
148
|
+
identityKey: conn.identityKey || "",
|
|
149
|
+
lastKnownStatus: "active",
|
|
132
150
|
});
|
|
133
151
|
const me = await hubRequestJson(normalizeHubUrl(hubAddress), result.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
134
152
|
return {
|
|
@@ -169,12 +187,10 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
169
187
|
const latestRole = String(me.role ?? "member") as UserRole;
|
|
170
188
|
if (conn && (conn.username !== latestUsername || conn.role !== latestRole)) {
|
|
171
189
|
store.setClientHubConnection({
|
|
172
|
-
|
|
173
|
-
userId: conn.userId,
|
|
190
|
+
...conn,
|
|
174
191
|
username: latestUsername,
|
|
175
|
-
userToken: conn.userToken,
|
|
176
192
|
role: latestRole,
|
|
177
|
-
|
|
193
|
+
lastKnownStatus: "active",
|
|
178
194
|
});
|
|
179
195
|
}
|
|
180
196
|
return {
|
|
@@ -190,7 +206,11 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
190
206
|
} catch (err: any) {
|
|
191
207
|
const is401 = typeof err?.message === "string" && err.message.includes("(401)");
|
|
192
208
|
if (is401 && conn) {
|
|
193
|
-
store.
|
|
209
|
+
store.setClientHubConnection({
|
|
210
|
+
...conn,
|
|
211
|
+
userToken: "",
|
|
212
|
+
lastKnownStatus: "removed",
|
|
213
|
+
});
|
|
194
214
|
return {
|
|
195
215
|
connected: false,
|
|
196
216
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
@@ -232,12 +252,17 @@ export async function autoJoinHub(
|
|
|
232
252
|
}
|
|
233
253
|
}
|
|
234
254
|
|
|
255
|
+
const persisted = store.getClientHubConnection();
|
|
256
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
257
|
+
|
|
235
258
|
log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
|
|
236
259
|
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
237
260
|
method: "POST",
|
|
238
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp }),
|
|
261
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp, identityKey: existingIdentityKey }),
|
|
239
262
|
}) as any;
|
|
240
263
|
|
|
264
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
265
|
+
|
|
241
266
|
if (result.status === "pending") {
|
|
242
267
|
log.info(`Join request submitted, awaiting admin approval. userId=${result.userId}`);
|
|
243
268
|
store.setClientHubConnection({
|
|
@@ -247,6 +272,8 @@ export async function autoJoinHub(
|
|
|
247
272
|
userToken: "",
|
|
248
273
|
role: "member",
|
|
249
274
|
connectedAt: Date.now(),
|
|
275
|
+
identityKey: returnedIdentityKey,
|
|
276
|
+
lastKnownStatus: "pending",
|
|
250
277
|
});
|
|
251
278
|
throw new PendingApprovalError(result.userId);
|
|
252
279
|
}
|
|
@@ -255,6 +282,10 @@ export async function autoJoinHub(
|
|
|
255
282
|
throw new Error(`Join request was rejected by the Hub admin.`);
|
|
256
283
|
}
|
|
257
284
|
|
|
285
|
+
if (result.status === "blocked") {
|
|
286
|
+
throw new Error(`Your account has been blocked by the Hub admin.`);
|
|
287
|
+
}
|
|
288
|
+
|
|
258
289
|
if (!result.userToken) {
|
|
259
290
|
throw new Error(`Hub join failed: ${JSON.stringify(result)}`);
|
|
260
291
|
}
|
|
@@ -267,6 +298,8 @@ export async function autoJoinHub(
|
|
|
267
298
|
userToken: result.userToken,
|
|
268
299
|
role: "member",
|
|
269
300
|
connectedAt: Date.now(),
|
|
301
|
+
identityKey: returnedIdentityKey,
|
|
302
|
+
lastKnownStatus: "active",
|
|
270
303
|
});
|
|
271
304
|
return store.getClientHubConnection()!;
|
|
272
305
|
}
|