@memtensor/memos-local-openclaw-plugin 1.0.4-beta.2 → 1.0.4-beta.20
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 +111 -44
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +36 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +6 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +160 -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 +2 -3
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +9 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +500 -112
- 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/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.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 +96 -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 +84 -9
- 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 +115 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +458 -110
- 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 +2952 -910
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +39 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1198 -227
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +774 -74
- package/openclaw.plugin.json +2 -2
- 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 +40 -1
- package/src/client/connector.ts +161 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +2 -3
- package/src/hub/server.ts +481 -107
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +99 -10
- 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 +498 -137
- 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 +2952 -910
- package/src/viewer/server.ts +1109 -212
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
|
}
|
package/src/ingest/chunker.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export type ChunkKind = "paragraph" | "code_block" | "error_stack" | "list" | "command";
|
|
2
|
+
|
|
1
3
|
export interface RawChunk {
|
|
2
4
|
content: string;
|
|
3
|
-
kind:
|
|
5
|
+
kind: ChunkKind;
|
|
4
6
|
}
|
|
5
7
|
|
|
6
8
|
const MAX_CHUNK_CHARS = 3000;
|
|
@@ -28,21 +30,25 @@ const COMMAND_LINE_RE = /^(?:\$|>|#)\s+.+$/gm;
|
|
|
28
30
|
*/
|
|
29
31
|
export function chunkText(text: string): RawChunk[] {
|
|
30
32
|
let remaining = text;
|
|
31
|
-
const slots: Array<{ placeholder: string; content: string }> = [];
|
|
33
|
+
const slots: Array<{ placeholder: string; content: string; kind: ChunkKind }> = [];
|
|
32
34
|
let counter = 0;
|
|
33
35
|
|
|
34
|
-
function ph(content: string): string {
|
|
36
|
+
function ph(content: string, kind: ChunkKind = "paragraph"): string {
|
|
35
37
|
const tag = `\x00SLOT_${counter++}\x00`;
|
|
36
|
-
slots.push({ placeholder: tag, content: content.trim() });
|
|
38
|
+
slots.push({ placeholder: tag, content: content.trim(), kind });
|
|
37
39
|
return tag;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m));
|
|
42
|
+
remaining = remaining.replace(FENCED_CODE_RE, (m) => ph(m, "code_block"));
|
|
41
43
|
remaining = extractBraceBlocks(remaining, ph);
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const structuralKinds: Array<[RegExp, ChunkKind]> = [
|
|
46
|
+
[ERROR_STACK_RE, "error_stack"],
|
|
47
|
+
[LIST_BLOCK_RE, "list"],
|
|
48
|
+
[COMMAND_LINE_RE, "command"],
|
|
49
|
+
];
|
|
50
|
+
for (const [re, kind] of structuralKinds) {
|
|
51
|
+
remaining = remaining.replace(re, (m) => ph(m, kind));
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
const raw: RawChunk[] = [];
|
|
@@ -57,7 +63,7 @@ export function chunkText(text: string): RawChunk[] {
|
|
|
57
63
|
for (const part of parts) {
|
|
58
64
|
const slot = slots.find((s) => s.placeholder === part);
|
|
59
65
|
if (slot) {
|
|
60
|
-
raw.push({ content: slot.content, kind:
|
|
66
|
+
raw.push({ content: slot.content, kind: slot.kind });
|
|
61
67
|
} else if (part.trim().length >= MIN_CHUNK_CHARS) {
|
|
62
68
|
raw.push({ content: part.trim(), kind: "paragraph" });
|
|
63
69
|
}
|
|
@@ -69,7 +75,7 @@ export function chunkText(text: string): RawChunk[] {
|
|
|
69
75
|
|
|
70
76
|
for (const s of slots) {
|
|
71
77
|
if (!raw.some((c) => c.content === s.content)) {
|
|
72
|
-
raw.push({ content: s.content, kind:
|
|
78
|
+
raw.push({ content: s.content, kind: s.kind });
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -85,7 +91,7 @@ export function chunkText(text: string): RawChunk[] {
|
|
|
85
91
|
*/
|
|
86
92
|
function extractBraceBlocks(
|
|
87
93
|
text: string,
|
|
88
|
-
ph: (content: string) => string,
|
|
94
|
+
ph: (content: string, kind?: ChunkKind) => string,
|
|
89
95
|
): string {
|
|
90
96
|
const lines = text.split("\n");
|
|
91
97
|
const result: string[] = [];
|
|
@@ -119,7 +125,7 @@ function extractBraceBlocks(
|
|
|
119
125
|
if (depth <= 0 || (BLOCK_CLOSE_RE.test(line) && depth <= 0)) {
|
|
120
126
|
const block = blockLines.join("\n");
|
|
121
127
|
if (block.trim().length >= MIN_CHUNK_CHARS) {
|
|
122
|
-
result.push(ph(block));
|
|
128
|
+
result.push(ph(block, "code_block"));
|
|
123
129
|
} else {
|
|
124
130
|
result.push(block);
|
|
125
131
|
}
|
|
@@ -135,7 +141,7 @@ function extractBraceBlocks(
|
|
|
135
141
|
if (blockLines.length > 0) {
|
|
136
142
|
const block = blockLines.join("\n");
|
|
137
143
|
if (block.trim().length >= MIN_CHUNK_CHARS) {
|
|
138
|
-
result.push(ph(block));
|
|
144
|
+
result.push(ph(block, "code_block"));
|
|
139
145
|
} else {
|
|
140
146
|
result.push(block);
|
|
141
147
|
}
|
|
@@ -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 cfgPath =
|
|
52
|
+
const cfgPath = process.env.OPENCLAW_CONFIG_PATH
|
|
53
|
+
|| path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "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,63 @@ export class RecallEngine {
|
|
|
74
74
|
score: 1 / (i + 1),
|
|
75
75
|
}));
|
|
76
76
|
|
|
77
|
+
// Step 1c: Hub memories search — only in Hub mode where local DB owns the
|
|
78
|
+
// hub_memories data and embeddings were generated by the same Embedder.
|
|
79
|
+
// Client mode must use remote API (hubSearchMemories) to avoid cross-model
|
|
80
|
+
// embedding mismatch.
|
|
81
|
+
let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
|
|
82
|
+
let hubMemVecRanked: Array<{ id: string; score: number }> = [];
|
|
83
|
+
let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
|
|
84
|
+
if (query && this.ctx.config.sharing?.enabled && this.ctx.config.sharing.role === "hub") {
|
|
85
|
+
try {
|
|
86
|
+
const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
|
|
87
|
+
hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
|
|
88
|
+
id: `hubmem:${hit.id}`, score: 1 / (i + 1),
|
|
89
|
+
}));
|
|
90
|
+
} catch { /* hub_memories table may not exist */ }
|
|
91
|
+
if (shortTerms.length > 0) {
|
|
92
|
+
try {
|
|
93
|
+
const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
|
|
94
|
+
hubMemPatternRanked = hubPatternHits.map((h, i) => ({
|
|
95
|
+
id: `hubmem:${h.memoryId}`, score: 1 / (i + 1),
|
|
96
|
+
}));
|
|
97
|
+
} catch { /* best-effort */ }
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const hubMemEmbs = this.store.getVisibleHubMemoryEmbeddings("");
|
|
101
|
+
if (hubMemEmbs.length > 0) {
|
|
102
|
+
const qv = await this.embedder.embedQuery(query).catch(() => null);
|
|
103
|
+
if (qv) {
|
|
104
|
+
const scored: Array<{ id: string; score: number }> = [];
|
|
105
|
+
for (const e of hubMemEmbs) {
|
|
106
|
+
let dot = 0, nA = 0, nB = 0;
|
|
107
|
+
for (let i = 0; i < qv.length && i < e.vector.length; i++) {
|
|
108
|
+
dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
|
|
109
|
+
}
|
|
110
|
+
const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
111
|
+
if (sim > 0.3) {
|
|
112
|
+
scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
scored.sort((a, b) => b.score - a.score);
|
|
116
|
+
hubMemVecRanked = scored.slice(0, candidatePool);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch { /* best-effort */ }
|
|
120
|
+
const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
|
|
121
|
+
if (hubTotal > 0) {
|
|
122
|
+
this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
77
126
|
// Step 2: RRF fusion
|
|
78
127
|
const ftsRanked = ftsCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
|
|
79
128
|
const vecRanked = vecCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
|
|
80
|
-
const
|
|
129
|
+
const allRankedLists = [ftsRanked, vecRanked, patternRanked];
|
|
130
|
+
if (hubMemFtsRanked.length > 0) allRankedLists.push(hubMemFtsRanked);
|
|
131
|
+
if (hubMemVecRanked.length > 0) allRankedLists.push(hubMemVecRanked);
|
|
132
|
+
if (hubMemPatternRanked.length > 0) allRankedLists.push(hubMemPatternRanked);
|
|
133
|
+
const rrfScores = rrfFuse(allRankedLists, recallCfg.rrfK);
|
|
81
134
|
|
|
82
135
|
if (rrfScores.size === 0) {
|
|
83
136
|
this.recordQuery(query, maxResults, minScore, 0);
|
|
@@ -101,6 +154,11 @@ export class RecallEngine {
|
|
|
101
154
|
|
|
102
155
|
// Step 4: Time decay
|
|
103
156
|
const withTs = mmrResults.map((r) => {
|
|
157
|
+
if (r.id.startsWith("hubmem:")) {
|
|
158
|
+
const memId = r.id.slice(7);
|
|
159
|
+
const mem = this.store.getHubMemoryById(memId);
|
|
160
|
+
return { ...r, createdAt: mem?.createdAt ?? 0 };
|
|
161
|
+
}
|
|
104
162
|
const chunk = this.store.getChunk(r.id);
|
|
105
163
|
return { ...r, createdAt: chunk?.createdAt ?? 0 };
|
|
106
164
|
});
|
|
@@ -128,6 +186,35 @@ export class RecallEngine {
|
|
|
128
186
|
const hits: SearchHit[] = [];
|
|
129
187
|
for (const candidate of normalized) {
|
|
130
188
|
if (hits.length >= maxResults) break;
|
|
189
|
+
|
|
190
|
+
if (candidate.id.startsWith("hubmem:")) {
|
|
191
|
+
const memId = candidate.id.slice(7);
|
|
192
|
+
const mem = this.store.getHubMemoryById(memId);
|
|
193
|
+
if (!mem) continue;
|
|
194
|
+
if (roleFilter && mem.role !== roleFilter) continue;
|
|
195
|
+
hits.push({
|
|
196
|
+
summary: mem.summary || mem.content.slice(0, 200),
|
|
197
|
+
original_excerpt: mem.content,
|
|
198
|
+
ref: {
|
|
199
|
+
sessionKey: `hub-shared:${mem.sourceUserId}`,
|
|
200
|
+
chunkId: mem.id,
|
|
201
|
+
turnId: "",
|
|
202
|
+
seq: 0,
|
|
203
|
+
},
|
|
204
|
+
score: Math.round(candidate.score * 1000) / 1000,
|
|
205
|
+
taskId: null,
|
|
206
|
+
skillId: null,
|
|
207
|
+
owner: `hub-user:${mem.sourceUserId}`,
|
|
208
|
+
origin: "hub-memory",
|
|
209
|
+
source: {
|
|
210
|
+
ts: mem.createdAt,
|
|
211
|
+
role: (mem.role || "assistant") as any,
|
|
212
|
+
sessionKey: `hub-shared:${mem.sourceUserId}`,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
131
218
|
const chunk = this.store.getChunk(candidate.id);
|
|
132
219
|
if (!chunk) continue;
|
|
133
220
|
if (roleFilter && chunk.role !== roleFilter) continue;
|
|
@@ -145,6 +232,7 @@ export class RecallEngine {
|
|
|
145
232
|
score: Math.round(candidate.score * 1000) / 1000,
|
|
146
233
|
taskId: chunk.taskId,
|
|
147
234
|
skillId: chunk.skillId,
|
|
235
|
+
origin: chunk.owner === "public" ? "local-shared" : "local",
|
|
148
236
|
source: {
|
|
149
237
|
ts: chunk.createdAt,
|
|
150
238
|
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).
|
|
@@ -9,7 +37,8 @@ import type { SummarizerConfig, Logger, PluginContext, OpenClawAPI } from "../ty
|
|
|
9
37
|
export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
|
|
10
38
|
try {
|
|
11
39
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
12
|
-
const cfgPath =
|
|
40
|
+
const cfgPath = process.env.OPENCLAW_CONFIG_PATH
|
|
41
|
+
|| path.join(process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw"), "openclaw.json");
|
|
13
42
|
if (!fs.existsSync(cfgPath)) return undefined;
|
|
14
43
|
|
|
15
44
|
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
@@ -30,13 +59,12 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
|
|
|
30
59
|
const apiKey: string | undefined = providerCfg.apiKey;
|
|
31
60
|
if (!baseUrl || !apiKey) return undefined;
|
|
32
61
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
: baseUrl.replace(/\/+$/, "") + "/chat/completions";
|
|
62
|
+
const provider = detectProvider(providerKey, baseUrl);
|
|
63
|
+
const endpoint = defaultEndpointForProvider(provider, baseUrl);
|
|
36
64
|
|
|
37
|
-
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
|
|
65
|
+
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
|
|
38
66
|
return {
|
|
39
|
-
provider
|
|
67
|
+
provider,
|
|
40
68
|
endpoint,
|
|
41
69
|
apiKey,
|
|
42
70
|
model: modelId,
|
|
@@ -70,23 +98,34 @@ export interface LLMCallOptions {
|
|
|
70
98
|
openclawAPI?: OpenClawAPI;
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
function
|
|
101
|
+
function normalizeOpenAIEndpoint(url: string): string {
|
|
74
102
|
const stripped = url.replace(/\/+$/, "");
|
|
75
103
|
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
76
104
|
if (stripped.endsWith("/completions")) return stripped;
|
|
77
105
|
return `${stripped}/chat/completions`;
|
|
78
106
|
}
|
|
79
107
|
|
|
108
|
+
function normalizeAnthropicEndpoint(url: string): string {
|
|
109
|
+
const stripped = url.replace(/\/+$/, "");
|
|
110
|
+
if (stripped.endsWith("/v1/messages")) return stripped;
|
|
111
|
+
if (stripped.endsWith("/messages")) return stripped;
|
|
112
|
+
return `${stripped}/v1/messages`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isAnthropicProvider(cfg: SummarizerConfig): boolean {
|
|
116
|
+
return cfg.provider === "anthropic";
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
/**
|
|
81
120
|
* Make a single LLM call with the given config. Throws on failure.
|
|
82
121
|
* When cfg.provider === "openclaw", delegates to the OpenClaw host completion API.
|
|
122
|
+
* Dispatches to Anthropic or OpenAI-compatible format based on provider.
|
|
83
123
|
*/
|
|
84
124
|
export async function callLLMOnce(
|
|
85
125
|
cfg: SummarizerConfig,
|
|
86
126
|
prompt: string,
|
|
87
127
|
opts: LLMCallOptions = {},
|
|
88
128
|
): Promise<string> {
|
|
89
|
-
// Handle openclaw provider via host completion API
|
|
90
129
|
if (cfg.provider === "openclaw") {
|
|
91
130
|
const api = opts.openclawAPI;
|
|
92
131
|
if (!api) {
|
|
@@ -101,7 +140,57 @@ export async function callLLMOnce(
|
|
|
101
140
|
return response.text.trim();
|
|
102
141
|
}
|
|
103
142
|
|
|
104
|
-
|
|
143
|
+
if (isAnthropicProvider(cfg)) {
|
|
144
|
+
return callLLMOnceAnthropic(cfg, prompt, opts);
|
|
145
|
+
}
|
|
146
|
+
return callLLMOnceOpenAI(cfg, prompt, opts);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function callLLMOnceAnthropic(
|
|
150
|
+
cfg: SummarizerConfig,
|
|
151
|
+
prompt: string,
|
|
152
|
+
opts: LLMCallOptions = {},
|
|
153
|
+
): Promise<string> {
|
|
154
|
+
const endpoint = normalizeAnthropicEndpoint(
|
|
155
|
+
cfg.endpoint ?? "https://api.anthropic.com/v1/messages",
|
|
156
|
+
);
|
|
157
|
+
const model = cfg.model ?? "claude-3-haiku-20240307";
|
|
158
|
+
const headers: Record<string, string> = {
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
"x-api-key": cfg.apiKey ?? "",
|
|
161
|
+
"anthropic-version": "2023-06-01",
|
|
162
|
+
...cfg.headers,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const resp = await fetch(endpoint, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers,
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
model,
|
|
170
|
+
temperature: opts.temperature ?? 0.1,
|
|
171
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
172
|
+
messages: [{ role: "user", content: prompt }],
|
|
173
|
+
}),
|
|
174
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!resp.ok) {
|
|
178
|
+
const body = await resp.text();
|
|
179
|
+
throw new Error(`LLM call failed (${resp.status}): ${body}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
|
|
183
|
+
return json.content.find((c) => c.type === "text")?.text?.trim() ?? "";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function callLLMOnceOpenAI(
|
|
187
|
+
cfg: SummarizerConfig,
|
|
188
|
+
prompt: string,
|
|
189
|
+
opts: LLMCallOptions = {},
|
|
190
|
+
): Promise<string> {
|
|
191
|
+
const endpoint = normalizeOpenAIEndpoint(
|
|
192
|
+
cfg.endpoint ?? "https://api.openai.com/v1/chat/completions",
|
|
193
|
+
);
|
|
105
194
|
const model = cfg.model ?? "gpt-4o-mini";
|
|
106
195
|
const headers: Record<string, string> = {
|
|
107
196
|
"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
|
|