@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 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/.env.example +7 -0
- package/README.md +24 -24
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +34 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +2 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +122 -26
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -2
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +8 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +390 -106
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +11 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +31 -3
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +93 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +82 -8
- package/dist/shared/llm-call.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 +4 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +59 -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/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +89 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +374 -124
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +11 -2
- 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 +2671 -879
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +30 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +990 -198
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +700 -56
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +37 -1
- package/src/client/connector.ts +124 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +0 -2
- package/src/hub/server.ts +374 -97
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +86 -1
- package/src/shared/llm-call.ts +97 -9
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +63 -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/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +395 -148
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +12 -2
- package/src/viewer/html.ts +2671 -879
- package/src/viewer/server.ts +913 -182
package/src/hub/user-manager.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "crypto";
|
|
2
|
-
import { issueUserToken } from "./auth";
|
|
2
|
+
import { issueUserToken, verifyUserToken } from "./auth";
|
|
3
3
|
import type { Logger } from "../types";
|
|
4
4
|
import type { UserInfo } from "../sharing/types";
|
|
5
5
|
import type { SqliteStore } from "../storage/sqlite";
|
|
6
6
|
|
|
7
|
-
type ManagedHubUser = UserInfo & {
|
|
7
|
+
type ManagedHubUser = UserInfo & {
|
|
8
|
+
tokenHash: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
approvedAt: number | null;
|
|
11
|
+
lastIp: string;
|
|
12
|
+
lastActiveAt: number | null;
|
|
13
|
+
identityKey?: string;
|
|
14
|
+
leftAt?: number | null;
|
|
15
|
+
removedAt?: number | null;
|
|
16
|
+
rejectedAt?: number | null;
|
|
17
|
+
rejoinRequestedAt?: number | null;
|
|
18
|
+
};
|
|
8
19
|
|
|
9
20
|
export class HubUserManager {
|
|
10
21
|
constructor(private store: SqliteStore, private log: Logger) {}
|
|
11
22
|
|
|
12
|
-
createPendingUser(input: { username: string; deviceName?: string }): ManagedHubUser {
|
|
13
|
-
const user = {
|
|
23
|
+
createPendingUser(input: { username: string; deviceName?: string; identityKey?: string }): ManagedHubUser {
|
|
24
|
+
const user: ManagedHubUser = {
|
|
14
25
|
id: randomUUID(),
|
|
15
26
|
username: input.username,
|
|
16
27
|
deviceName: input.deviceName,
|
|
@@ -20,11 +31,38 @@ export class HubUserManager {
|
|
|
20
31
|
tokenHash: "",
|
|
21
32
|
createdAt: Date.now(),
|
|
22
33
|
approvedAt: null,
|
|
34
|
+
lastIp: "",
|
|
35
|
+
lastActiveAt: null,
|
|
36
|
+
identityKey: input.identityKey || "",
|
|
23
37
|
};
|
|
24
38
|
this.store.upsertHubUser(user);
|
|
25
39
|
return user;
|
|
26
40
|
}
|
|
27
41
|
|
|
42
|
+
findByIdentityKey(identityKey: string): ManagedHubUser | null {
|
|
43
|
+
if (!identityKey) return null;
|
|
44
|
+
return this.store.findHubUserByIdentityKey(identityKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
markUserLeft(userId: string): boolean {
|
|
48
|
+
this.log.info(`Hub: user "${userId}" marked as left`);
|
|
49
|
+
return this.store.markHubUserLeft(userId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
rejoinUser(userId: string): ManagedHubUser | null {
|
|
53
|
+
const user = this.store.getHubUser(userId);
|
|
54
|
+
if (!user) return null;
|
|
55
|
+
const updated: ManagedHubUser = {
|
|
56
|
+
...user,
|
|
57
|
+
status: "pending" as const,
|
|
58
|
+
tokenHash: "",
|
|
59
|
+
rejoinRequestedAt: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
this.store.upsertHubUser(updated);
|
|
62
|
+
this.log.info(`Hub: user "${userId}" (${user.username}) requested rejoin, previous status: ${user.status}`);
|
|
63
|
+
return updated;
|
|
64
|
+
}
|
|
65
|
+
|
|
28
66
|
listPendingUsers(): ManagedHubUser[] {
|
|
29
67
|
return this.store.listHubUsers("pending");
|
|
30
68
|
}
|
|
@@ -46,7 +84,7 @@ export class HubUserManager {
|
|
|
46
84
|
if (bootstrapUserId) {
|
|
47
85
|
const bootstrapUser = this.store.getHubUser(bootstrapUserId);
|
|
48
86
|
if (bootstrapUser && bootstrapUser.role === "admin" && bootstrapUser.status === "active") {
|
|
49
|
-
if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex")) {
|
|
87
|
+
if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex") && verifyUserToken(bootstrapToken, secret)) {
|
|
50
88
|
return { user: bootstrapUser, token: bootstrapToken };
|
|
51
89
|
}
|
|
52
90
|
const refreshedToken = issueUserToken(
|
|
@@ -88,6 +126,8 @@ export class HubUserManager {
|
|
|
88
126
|
tokenHash: "",
|
|
89
127
|
createdAt: Date.now(),
|
|
90
128
|
approvedAt: Date.now(),
|
|
129
|
+
lastIp: "",
|
|
130
|
+
lastActiveAt: null,
|
|
91
131
|
};
|
|
92
132
|
const token = issueUserToken(
|
|
93
133
|
{ userId: user.id, username: user.username, role: user.role, status: user.status },
|
|
@@ -101,7 +141,7 @@ export class HubUserManager {
|
|
|
101
141
|
|
|
102
142
|
isUsernameTaken(username: string, excludeUserId?: string): boolean {
|
|
103
143
|
const users = this.store.listHubUsers();
|
|
104
|
-
return users.some(u => u.username === username && u.id !== excludeUserId);
|
|
144
|
+
return users.some(u => u.username === username && u.id !== excludeUserId && u.status !== "left" && u.status !== "removed");
|
|
105
145
|
}
|
|
106
146
|
|
|
107
147
|
updateUsername(userId: string, newUsername: string): ManagedHubUser | null {
|
|
@@ -115,10 +155,10 @@ export class HubUserManager {
|
|
|
115
155
|
rejectUser(userId: string): ManagedHubUser | null {
|
|
116
156
|
const user = this.store.getHubUser(userId);
|
|
117
157
|
if (!user) return null;
|
|
118
|
-
const updated = {
|
|
158
|
+
const updated: ManagedHubUser = {
|
|
119
159
|
...user,
|
|
120
160
|
status: "rejected" as const,
|
|
121
|
-
|
|
161
|
+
rejectedAt: Date.now(),
|
|
122
162
|
};
|
|
123
163
|
this.store.upsertHubUser(updated);
|
|
124
164
|
return updated;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
import { buildContext } from "./config";
|
|
3
|
+
import { ensureSqliteBinding } from "./storage/ensure-binding";
|
|
3
4
|
import { SqliteStore } from "./storage/sqlite";
|
|
4
5
|
import { Embedder } from "./embedding";
|
|
5
6
|
import { IngestWorker } from "./ingest/worker";
|
|
@@ -56,13 +57,17 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
|
|
|
56
57
|
|
|
57
58
|
ctx.log.info("Initializing memos-local plugin...");
|
|
58
59
|
|
|
60
|
+
ensureSqliteBinding(ctx.log);
|
|
61
|
+
|
|
59
62
|
const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
|
|
60
63
|
const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
|
|
61
64
|
const worker = new IngestWorker(store, embedder, ctx);
|
|
62
65
|
const engine = new RecallEngine(store, embedder, ctx);
|
|
63
66
|
|
|
67
|
+
const sharedState = { lastSearchTime: 0 };
|
|
68
|
+
|
|
64
69
|
const tools: ToolDefinition[] = [
|
|
65
|
-
createMemorySearchTool(engine, store, ctx),
|
|
70
|
+
createMemorySearchTool(engine, store, ctx, sharedState),
|
|
66
71
|
createMemoryTimelineTool(store),
|
|
67
72
|
createMemoryGetTool(store),
|
|
68
73
|
createNetworkMemoryDetailTool(store, ctx),
|
|
@@ -84,7 +89,10 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
|
|
|
84
89
|
const turnId = uuid();
|
|
85
90
|
const tag = ctx.config.capture?.evidenceWrapperTag ?? "STORED_MEMORY";
|
|
86
91
|
|
|
87
|
-
const
|
|
92
|
+
const userSearchTime = sharedState.lastSearchTime || 0;
|
|
93
|
+
sharedState.lastSearchTime = 0;
|
|
94
|
+
|
|
95
|
+
const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner, userSearchTime);
|
|
88
96
|
if (captured.length > 0) {
|
|
89
97
|
worker.enqueue(captured);
|
|
90
98
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import type { SummarizerConfig, Logger, OpenClawAPI } from "../../types";
|
|
3
|
+
import type { SummarizerConfig, SummaryProvider, Logger, OpenClawAPI } from "../../types";
|
|
4
4
|
import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI, parseFilterResult, parseDedupResult } from "./openai";
|
|
5
5
|
import type { FilterResult, DedupResult } from "./openai";
|
|
6
6
|
export type { FilterResult, DedupResult } from "./openai";
|
|
@@ -8,6 +8,40 @@ import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic,
|
|
|
8
8
|
import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
|
|
9
9
|
import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Detect provider type from provider key name or base URL.
|
|
13
|
+
*/
|
|
14
|
+
function detectProvider(
|
|
15
|
+
providerKey: string | undefined,
|
|
16
|
+
baseUrl: string,
|
|
17
|
+
): SummaryProvider {
|
|
18
|
+
const key = providerKey?.toLowerCase() ?? "";
|
|
19
|
+
const url = baseUrl.toLowerCase();
|
|
20
|
+
if (key.includes("anthropic") || url.includes("anthropic")) return "anthropic";
|
|
21
|
+
if (key.includes("gemini") || url.includes("generativelanguage.googleapis.com")) {
|
|
22
|
+
return "gemini";
|
|
23
|
+
}
|
|
24
|
+
if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock";
|
|
25
|
+
return "openai_compatible";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return the correct endpoint for a given provider and base URL.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeEndpointForProvider(
|
|
32
|
+
provider: SummaryProvider,
|
|
33
|
+
baseUrl: string,
|
|
34
|
+
): string {
|
|
35
|
+
const stripped = baseUrl.replace(/\/+$/, "");
|
|
36
|
+
if (provider === "anthropic") {
|
|
37
|
+
if (stripped.endsWith("/v1/messages")) return stripped;
|
|
38
|
+
return `${stripped}/v1/messages`;
|
|
39
|
+
}
|
|
40
|
+
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
41
|
+
if (stripped.endsWith("/completions")) return stripped;
|
|
42
|
+
return `${stripped}/chat/completions`;
|
|
43
|
+
}
|
|
44
|
+
|
|
11
45
|
/**
|
|
12
46
|
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
|
|
13
47
|
* This serves as the final fallback when both strongCfg and plugin summarizer fail or are absent.
|
|
@@ -15,7 +49,8 @@ import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judge
|
|
|
15
49
|
function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
|
|
16
50
|
try {
|
|
17
51
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
18
|
-
const
|
|
52
|
+
const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
|
|
53
|
+
const cfgPath = path.join(ocHome, "openclaw.json");
|
|
19
54
|
if (!fs.existsSync(cfgPath)) return undefined;
|
|
20
55
|
|
|
21
56
|
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
@@ -36,13 +71,12 @@ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
|
|
|
36
71
|
const apiKey: string | undefined = providerCfg.apiKey;
|
|
37
72
|
if (!baseUrl || !apiKey) return undefined;
|
|
38
73
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
: baseUrl.replace(/\/+$/, "") + "/chat/completions";
|
|
74
|
+
const provider = detectProvider(providerKey, baseUrl);
|
|
75
|
+
const endpoint = normalizeEndpointForProvider(provider, baseUrl);
|
|
42
76
|
|
|
43
|
-
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
|
|
77
|
+
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
|
|
44
78
|
return {
|
|
45
|
-
provider
|
|
79
|
+
provider,
|
|
46
80
|
endpoint,
|
|
47
81
|
apiKey,
|
|
48
82
|
model: modelId,
|
package/src/recall/engine.ts
CHANGED
|
@@ -74,10 +74,60 @@ export class RecallEngine {
|
|
|
74
74
|
score: 1 / (i + 1),
|
|
75
75
|
}));
|
|
76
76
|
|
|
77
|
+
// Step 1c: Hub memories search (when sharing is enabled and hub_memories exist)
|
|
78
|
+
let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
|
|
79
|
+
let hubMemVecRanked: Array<{ id: string; score: number }> = [];
|
|
80
|
+
let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
|
|
81
|
+
if (query && this.ctx.config.sharing?.enabled) {
|
|
82
|
+
try {
|
|
83
|
+
const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
|
|
84
|
+
hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
|
|
85
|
+
id: `hubmem:${hit.id}`, score: 1 / (i + 1),
|
|
86
|
+
}));
|
|
87
|
+
} catch { /* hub_memories table may not exist */ }
|
|
88
|
+
if (shortTerms.length > 0) {
|
|
89
|
+
try {
|
|
90
|
+
const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
|
|
91
|
+
hubMemPatternRanked = hubPatternHits.map((h, i) => ({
|
|
92
|
+
id: `hubmem:${h.memoryId}`, score: 1 / (i + 1),
|
|
93
|
+
}));
|
|
94
|
+
} catch { /* best-effort */ }
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const hubMemEmbs = this.store.getVisibleHubMemoryEmbeddings("");
|
|
98
|
+
if (hubMemEmbs.length > 0) {
|
|
99
|
+
const qv = await this.embedder.embedQuery(query).catch(() => null);
|
|
100
|
+
if (qv) {
|
|
101
|
+
const scored: Array<{ id: string; score: number }> = [];
|
|
102
|
+
for (const e of hubMemEmbs) {
|
|
103
|
+
let dot = 0, nA = 0, nB = 0;
|
|
104
|
+
for (let i = 0; i < qv.length && i < e.vector.length; i++) {
|
|
105
|
+
dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
|
|
106
|
+
}
|
|
107
|
+
const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
108
|
+
if (sim > 0.3) {
|
|
109
|
+
scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
scored.sort((a, b) => b.score - a.score);
|
|
113
|
+
hubMemVecRanked = scored.slice(0, candidatePool);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch { /* best-effort */ }
|
|
117
|
+
const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
|
|
118
|
+
if (hubTotal > 0) {
|
|
119
|
+
this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
77
123
|
// Step 2: RRF fusion
|
|
78
124
|
const ftsRanked = ftsCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
|
|
79
125
|
const vecRanked = vecCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
|
|
80
|
-
const
|
|
126
|
+
const allRankedLists = [ftsRanked, vecRanked, patternRanked];
|
|
127
|
+
if (hubMemFtsRanked.length > 0) allRankedLists.push(hubMemFtsRanked);
|
|
128
|
+
if (hubMemVecRanked.length > 0) allRankedLists.push(hubMemVecRanked);
|
|
129
|
+
if (hubMemPatternRanked.length > 0) allRankedLists.push(hubMemPatternRanked);
|
|
130
|
+
const rrfScores = rrfFuse(allRankedLists, recallCfg.rrfK);
|
|
81
131
|
|
|
82
132
|
if (rrfScores.size === 0) {
|
|
83
133
|
this.recordQuery(query, maxResults, minScore, 0);
|
|
@@ -101,6 +151,11 @@ export class RecallEngine {
|
|
|
101
151
|
|
|
102
152
|
// Step 4: Time decay
|
|
103
153
|
const withTs = mmrResults.map((r) => {
|
|
154
|
+
if (r.id.startsWith("hubmem:")) {
|
|
155
|
+
const memId = r.id.slice(7);
|
|
156
|
+
const mem = this.store.getHubMemoryById(memId);
|
|
157
|
+
return { ...r, createdAt: mem?.createdAt ?? 0 };
|
|
158
|
+
}
|
|
104
159
|
const chunk = this.store.getChunk(r.id);
|
|
105
160
|
return { ...r, createdAt: chunk?.createdAt ?? 0 };
|
|
106
161
|
});
|
|
@@ -128,6 +183,35 @@ export class RecallEngine {
|
|
|
128
183
|
const hits: SearchHit[] = [];
|
|
129
184
|
for (const candidate of normalized) {
|
|
130
185
|
if (hits.length >= maxResults) break;
|
|
186
|
+
|
|
187
|
+
if (candidate.id.startsWith("hubmem:")) {
|
|
188
|
+
const memId = candidate.id.slice(7);
|
|
189
|
+
const mem = this.store.getHubMemoryById(memId);
|
|
190
|
+
if (!mem) continue;
|
|
191
|
+
if (roleFilter && mem.role !== roleFilter) continue;
|
|
192
|
+
hits.push({
|
|
193
|
+
summary: mem.summary || mem.content.slice(0, 200),
|
|
194
|
+
original_excerpt: mem.content,
|
|
195
|
+
ref: {
|
|
196
|
+
sessionKey: `hub-shared:${mem.sourceUserId}`,
|
|
197
|
+
chunkId: mem.id,
|
|
198
|
+
turnId: "",
|
|
199
|
+
seq: 0,
|
|
200
|
+
},
|
|
201
|
+
score: Math.round(candidate.score * 1000) / 1000,
|
|
202
|
+
taskId: null,
|
|
203
|
+
skillId: null,
|
|
204
|
+
owner: `hub-user:${mem.sourceUserId}`,
|
|
205
|
+
origin: "hub-memory",
|
|
206
|
+
source: {
|
|
207
|
+
ts: mem.createdAt,
|
|
208
|
+
role: (mem.role || "assistant") as any,
|
|
209
|
+
sessionKey: `hub-shared:${mem.sourceUserId}`,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
131
215
|
const chunk = this.store.getChunk(candidate.id);
|
|
132
216
|
if (!chunk) continue;
|
|
133
217
|
if (roleFilter && chunk.role !== roleFilter) continue;
|
|
@@ -145,6 +229,7 @@ export class RecallEngine {
|
|
|
145
229
|
score: Math.round(candidate.score * 1000) / 1000,
|
|
146
230
|
taskId: chunk.taskId,
|
|
147
231
|
skillId: chunk.skillId,
|
|
232
|
+
origin: chunk.owner === "public" ? "local-shared" : "local",
|
|
148
233
|
source: {
|
|
149
234
|
ts: chunk.createdAt,
|
|
150
235
|
role: chunk.role,
|
package/src/shared/llm-call.ts
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import type { SummarizerConfig, Logger, PluginContext, OpenClawAPI } from "../types";
|
|
3
|
+
import type { SummarizerConfig, SummaryProvider, Logger, PluginContext, OpenClawAPI } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect provider type from provider key name or base URL.
|
|
7
|
+
*/
|
|
8
|
+
function detectProvider(providerKey: string | undefined, baseUrl: string): SummaryProvider {
|
|
9
|
+
const key = providerKey?.toLowerCase() ?? "";
|
|
10
|
+
const url = baseUrl.toLowerCase();
|
|
11
|
+
if (key.includes("anthropic") || url.includes("anthropic")) return "anthropic";
|
|
12
|
+
if (key.includes("gemini") || url.includes("generativelanguage.googleapis.com")) {
|
|
13
|
+
return "gemini";
|
|
14
|
+
}
|
|
15
|
+
if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock";
|
|
16
|
+
return "openai_compatible";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Return the correct default endpoint for a given provider.
|
|
21
|
+
*/
|
|
22
|
+
function defaultEndpointForProvider(provider: SummaryProvider, baseUrl: string): string {
|
|
23
|
+
const stripped = baseUrl.replace(/\/+$/, "");
|
|
24
|
+
if (provider === "anthropic") {
|
|
25
|
+
if (stripped.endsWith("/v1/messages")) return stripped;
|
|
26
|
+
return `${stripped}/v1/messages`;
|
|
27
|
+
}
|
|
28
|
+
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
29
|
+
if (stripped.endsWith("/completions")) return stripped;
|
|
30
|
+
return `${stripped}/chat/completions`;
|
|
31
|
+
}
|
|
4
32
|
|
|
5
33
|
/**
|
|
6
34
|
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
|
|
@@ -30,13 +58,12 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
|
|
|
30
58
|
const apiKey: string | undefined = providerCfg.apiKey;
|
|
31
59
|
if (!baseUrl || !apiKey) return undefined;
|
|
32
60
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
: baseUrl.replace(/\/+$/, "") + "/chat/completions";
|
|
61
|
+
const provider = detectProvider(providerKey, baseUrl);
|
|
62
|
+
const endpoint = defaultEndpointForProvider(provider, baseUrl);
|
|
36
63
|
|
|
37
|
-
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
|
|
64
|
+
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
|
|
38
65
|
return {
|
|
39
|
-
provider
|
|
66
|
+
provider,
|
|
40
67
|
endpoint,
|
|
41
68
|
apiKey,
|
|
42
69
|
model: modelId,
|
|
@@ -70,23 +97,34 @@ export interface LLMCallOptions {
|
|
|
70
97
|
openclawAPI?: OpenClawAPI;
|
|
71
98
|
}
|
|
72
99
|
|
|
73
|
-
function
|
|
100
|
+
function normalizeOpenAIEndpoint(url: string): string {
|
|
74
101
|
const stripped = url.replace(/\/+$/, "");
|
|
75
102
|
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
76
103
|
if (stripped.endsWith("/completions")) return stripped;
|
|
77
104
|
return `${stripped}/chat/completions`;
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
function normalizeAnthropicEndpoint(url: string): string {
|
|
108
|
+
const stripped = url.replace(/\/+$/, "");
|
|
109
|
+
if (stripped.endsWith("/v1/messages")) return stripped;
|
|
110
|
+
if (stripped.endsWith("/messages")) return stripped;
|
|
111
|
+
return `${stripped}/v1/messages`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isAnthropicProvider(cfg: SummarizerConfig): boolean {
|
|
115
|
+
return cfg.provider === "anthropic";
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
/**
|
|
81
119
|
* Make a single LLM call with the given config. Throws on failure.
|
|
82
120
|
* When cfg.provider === "openclaw", delegates to the OpenClaw host completion API.
|
|
121
|
+
* Dispatches to Anthropic or OpenAI-compatible format based on provider.
|
|
83
122
|
*/
|
|
84
123
|
export async function callLLMOnce(
|
|
85
124
|
cfg: SummarizerConfig,
|
|
86
125
|
prompt: string,
|
|
87
126
|
opts: LLMCallOptions = {},
|
|
88
127
|
): Promise<string> {
|
|
89
|
-
// Handle openclaw provider via host completion API
|
|
90
128
|
if (cfg.provider === "openclaw") {
|
|
91
129
|
const api = opts.openclawAPI;
|
|
92
130
|
if (!api) {
|
|
@@ -101,7 +139,57 @@ export async function callLLMOnce(
|
|
|
101
139
|
return response.text.trim();
|
|
102
140
|
}
|
|
103
141
|
|
|
104
|
-
|
|
142
|
+
if (isAnthropicProvider(cfg)) {
|
|
143
|
+
return callLLMOnceAnthropic(cfg, prompt, opts);
|
|
144
|
+
}
|
|
145
|
+
return callLLMOnceOpenAI(cfg, prompt, opts);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function callLLMOnceAnthropic(
|
|
149
|
+
cfg: SummarizerConfig,
|
|
150
|
+
prompt: string,
|
|
151
|
+
opts: LLMCallOptions = {},
|
|
152
|
+
): Promise<string> {
|
|
153
|
+
const endpoint = normalizeAnthropicEndpoint(
|
|
154
|
+
cfg.endpoint ?? "https://api.anthropic.com/v1/messages",
|
|
155
|
+
);
|
|
156
|
+
const model = cfg.model ?? "claude-3-haiku-20240307";
|
|
157
|
+
const headers: Record<string, string> = {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
"x-api-key": cfg.apiKey ?? "",
|
|
160
|
+
"anthropic-version": "2023-06-01",
|
|
161
|
+
...cfg.headers,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const resp = await fetch(endpoint, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers,
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
model,
|
|
169
|
+
temperature: opts.temperature ?? 0.1,
|
|
170
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
171
|
+
messages: [{ role: "user", content: prompt }],
|
|
172
|
+
}),
|
|
173
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!resp.ok) {
|
|
177
|
+
const body = await resp.text();
|
|
178
|
+
throw new Error(`LLM call failed (${resp.status}): ${body}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
|
|
182
|
+
return json.content.find((c) => c.type === "text")?.text?.trim() ?? "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function callLLMOnceOpenAI(
|
|
186
|
+
cfg: SummarizerConfig,
|
|
187
|
+
prompt: string,
|
|
188
|
+
opts: LLMCallOptions = {},
|
|
189
|
+
): Promise<string> {
|
|
190
|
+
const endpoint = normalizeOpenAIEndpoint(
|
|
191
|
+
cfg.endpoint ?? "https://api.openai.com/v1/chat/completions",
|
|
192
|
+
);
|
|
105
193
|
const model = cfg.model ?? "gpt-4o-mini";
|
|
106
194
|
const headers: Record<string, string> = {
|
|
107
195
|
"Content-Type": "application/json",
|
package/src/sharing/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
export type HubScope = "local" | "group" | "all";
|
|
13
13
|
export type SharedVisibility = "group" | "public";
|
|
14
14
|
export type UserRole = "admin" | "member";
|
|
15
|
-
export type UserStatus = "pending" | "active" | "blocked" | "rejected";
|
|
15
|
+
export type UserStatus = "pending" | "active" | "blocked" | "rejected" | "removed" | "left";
|
|
16
16
|
|
|
17
17
|
export type { ClientModeConfig, HubModeConfig, SharingCapabilities, SharingConfig, SharingRole };
|
|
18
18
|
|
package/src/skill/evolver.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { DEFAULTS } from "../types";
|
|
|
9
9
|
import { SkillEvaluator } from "./evaluator";
|
|
10
10
|
import { SkillGenerator } from "./generator";
|
|
11
11
|
import { SkillUpgrader } from "./upgrader";
|
|
12
|
-
import { SkillInstaller } from "./installer";
|
|
12
|
+
import { SkillInstaller, type SkillInstallMode } from "./installer";
|
|
13
13
|
import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
|
|
14
14
|
|
|
15
|
+
export type SkillEvolvedCallback = (skillName: string, upgradeType: "created" | "upgraded") => void;
|
|
16
|
+
|
|
15
17
|
export class SkillEvolver {
|
|
16
18
|
private evaluator: SkillEvaluator;
|
|
17
19
|
private generator: SkillGenerator;
|
|
@@ -19,6 +21,7 @@ export class SkillEvolver {
|
|
|
19
21
|
private installer: SkillInstaller;
|
|
20
22
|
private processing = false;
|
|
21
23
|
private queue: Task[] = [];
|
|
24
|
+
onSkillEvolved: SkillEvolvedCallback | null = null;
|
|
22
25
|
|
|
23
26
|
constructor(
|
|
24
27
|
private store: SqliteStore,
|
|
@@ -93,10 +96,19 @@ export class SkillEvolver {
|
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
const preferUpgrade = this.ctx.config.skillEvolution?.preferUpgradeExisting ?? DEFAULTS.skillPreferUpgrade;
|
|
96
100
|
const relatedSkill = await this.findRelatedSkill(task);
|
|
97
101
|
|
|
98
102
|
if (relatedSkill) {
|
|
99
103
|
await this.handleExistingSkill(task, chunks, relatedSkill);
|
|
104
|
+
} else if (preferUpgrade) {
|
|
105
|
+
const nameCandidate = await this.findSkillByNameSimilarity(task);
|
|
106
|
+
if (nameCandidate) {
|
|
107
|
+
this.ctx.log.info(`SkillEvolver: preferUpgrade found name-similar skill "${nameCandidate.name}" for task "${task.title}"`);
|
|
108
|
+
await this.handleExistingSkill(task, chunks, nameCandidate);
|
|
109
|
+
} else {
|
|
110
|
+
await this.handleNewSkill(task, chunks);
|
|
111
|
+
}
|
|
100
112
|
} else {
|
|
101
113
|
await this.handleNewSkill(task, chunks);
|
|
102
114
|
}
|
|
@@ -278,7 +290,12 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
278
290
|
|
|
279
291
|
if (upgraded) {
|
|
280
292
|
this.store.linkTaskSkill(task.id, freshSkill.id, "evolved_from", freshSkill.version + 1);
|
|
281
|
-
|
|
293
|
+
if (freshSkill.installed) {
|
|
294
|
+
this.installer.syncIfInstalled(freshSkill.name);
|
|
295
|
+
} else {
|
|
296
|
+
this.autoInstallIfNeeded(freshSkill);
|
|
297
|
+
}
|
|
298
|
+
this.onSkillEvolved?.(freshSkill.name, "upgraded");
|
|
282
299
|
} else {
|
|
283
300
|
this.store.linkTaskSkill(task.id, freshSkill.id, "applied_to", freshSkill.version);
|
|
284
301
|
}
|
|
@@ -300,6 +317,13 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
300
317
|
const evalResult = await this.evaluator.evaluateCreate(task);
|
|
301
318
|
|
|
302
319
|
if (evalResult.shouldGenerate && evalResult.confidence >= minConfidence) {
|
|
320
|
+
const existingByName = this.store.getSkillByName(evalResult.suggestedName);
|
|
321
|
+
if (existingByName && (existingByName.status === "active" || existingByName.status === "draft")) {
|
|
322
|
+
this.ctx.log.info(`SkillEvolver: skill "${evalResult.suggestedName}" already exists, redirecting to upgrade instead of create`);
|
|
323
|
+
await this.handleExistingSkill(task, chunks, existingByName);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
303
327
|
this.ctx.log.info(`SkillEvolver: generating new skill "${evalResult.suggestedName}" — ${evalResult.reason}`);
|
|
304
328
|
this.store.setTaskSkillMeta(task.id, { skillStatus: "generating", skillReason: evalResult.reason });
|
|
305
329
|
|
|
@@ -307,11 +331,9 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
307
331
|
this.markChunksWithSkill(chunks, skill.id);
|
|
308
332
|
this.store.linkTaskSkill(task.id, skill.id, "generated_from", 1);
|
|
309
333
|
this.store.setTaskSkillMeta(task.id, { skillStatus: "generated", skillReason: evalResult.reason });
|
|
334
|
+
this.onSkillEvolved?.(skill.name, "created");
|
|
310
335
|
|
|
311
|
-
|
|
312
|
-
if (autoInstall && skill.status === "active") {
|
|
313
|
-
this.installer.install(skill.id);
|
|
314
|
-
}
|
|
336
|
+
this.autoInstallIfNeeded(skill);
|
|
315
337
|
} else {
|
|
316
338
|
const reason = evalResult.reason || `confidence不足 (${evalResult.confidence} < ${minConfidence})`;
|
|
317
339
|
this.ctx.log.debug(`SkillEvolver: task "${task.title}" not worth generating skill — ${reason}`);
|
|
@@ -326,6 +348,41 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
326
348
|
this.ctx.log.debug(`SkillEvolver: marked ${chunks.length} chunks with skill_id=${skillId}`);
|
|
327
349
|
}
|
|
328
350
|
|
|
351
|
+
private async findSkillByNameSimilarity(task: Task): Promise<Skill | null> {
|
|
352
|
+
const query = task.title.slice(0, 200);
|
|
353
|
+
const owner = task.owner ?? "agent:main";
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const ftsHits = this.store.skillFtsSearch(query, 5, "mix", owner);
|
|
357
|
+
for (const hit of ftsHits) {
|
|
358
|
+
if (hit.score < 0.5) continue;
|
|
359
|
+
const skill = this.store.getSkill(hit.skillId);
|
|
360
|
+
if (skill && (skill.status === "active" || skill.status === "draft")) {
|
|
361
|
+
return skill;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch { /* best-effort */ }
|
|
365
|
+
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private autoInstallIfNeeded(skill: Skill): void {
|
|
370
|
+
if (skill.status !== "active") return;
|
|
371
|
+
|
|
372
|
+
const explicitAutoInstall = this.ctx.config.skillEvolution?.autoInstall ?? DEFAULTS.skillAutoInstall;
|
|
373
|
+
if (explicitAutoInstall) {
|
|
374
|
+
this.installer.install(skill.id);
|
|
375
|
+
this.ctx.log.info(`SkillEvolver: auto-installed "${skill.name}" (explicit autoInstall=true)`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const manifest = SkillInstaller.buildManifest(skill.dirPath, !!skill.installed, skill.name);
|
|
380
|
+
if (manifest.installMode === "install_recommended") {
|
|
381
|
+
this.installer.install(skill.id);
|
|
382
|
+
this.ctx.log.info(`SkillEvolver: auto-installed "${skill.name}" (install_recommended: ${manifest.scriptsCount} scripts, ${Math.round(manifest.totalSize / 1024)}KB)`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
329
386
|
private readSkillContent(skill: Skill): string | null {
|
|
330
387
|
const filePath = path.join(skill.dirPath, "SKILL.md");
|
|
331
388
|
try {
|