@memtensor/memos-local-openclaw-plugin 1.0.4-beta.7 → 1.0.4-beta.8
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/README.md +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +6 -0
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +17 -4
- package/dist/client/connector.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 +7 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +160 -5
- package/dist/hub/server.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/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/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +3 -0
- package/dist/skill/evolver.js.map +1 -1
- package/dist/storage/sqlite.d.ts +4 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +8 -4
- 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 +135 -38
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +473 -191
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +14 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +233 -20
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +26 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -2
- package/scripts/postinstall.cjs +1 -1
- package/src/capture/index.ts +8 -0
- package/src/client/connector.ts +17 -4
- package/src/config.ts +0 -2
- package/src/hub/server.ts +157 -5
- package/src/ingest/providers/index.ts +41 -7
- package/src/shared/llm-call.ts +97 -9
- package/src/skill/evolver.ts +5 -0
- package/src/storage/sqlite.ts +11 -6
- package/src/telemetry.ts +152 -39
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +473 -191
- package/src/viewer/server.ts +208 -20
package/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
12
13
|
import { buildContext } from "./src/config";
|
|
13
14
|
import type { HostModelsConfig } from "./src/openclaw-api";
|
|
14
15
|
import { ensureSqliteBinding } from "./src/storage/ensure-binding";
|
|
@@ -82,13 +83,20 @@ const memosLocalPlugin = {
|
|
|
82
83
|
|
|
83
84
|
register(api: OpenClawPluginApi) {
|
|
84
85
|
// ─── Ensure better-sqlite3 native module is available ───
|
|
85
|
-
const pluginDir = path.dirname(
|
|
86
|
+
const pluginDir = path.dirname(fileURLToPath(import.meta.url));
|
|
87
|
+
|
|
88
|
+
function normalizeFsPath(p: string): string {
|
|
89
|
+
return path.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
let sqliteReady = false;
|
|
87
93
|
|
|
88
94
|
function trySqliteLoad(): boolean {
|
|
89
95
|
try {
|
|
90
96
|
const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
|
|
91
|
-
|
|
97
|
+
const resolvedNorm = normalizeFsPath(resolved);
|
|
98
|
+
const pluginNorm = normalizeFsPath(pluginDir);
|
|
99
|
+
if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
|
|
92
100
|
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
|
|
93
101
|
return false;
|
|
94
102
|
}
|
|
@@ -208,6 +216,7 @@ const memosLocalPlugin = {
|
|
|
208
216
|
const workspaceDir = api.resolvePath("~/.openclaw/workspace");
|
|
209
217
|
const skillCtx = { ...ctx, workspaceDir };
|
|
210
218
|
const skillEvolver = new SkillEvolver(store, engine, skillCtx);
|
|
219
|
+
skillEvolver.onSkillEvolved = (name, type) => telemetry.trackSkillEvolved(name, type);
|
|
211
220
|
const skillInstaller = new SkillInstaller(store, skillCtx);
|
|
212
221
|
|
|
213
222
|
let pluginVersion = "0.0.0";
|
|
@@ -272,6 +281,18 @@ const memosLocalPlugin = {
|
|
|
272
281
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
273
282
|
let currentAgentId = "main";
|
|
274
283
|
|
|
284
|
+
// ─── Check allowPromptInjection policy ───
|
|
285
|
+
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
286
|
+
// will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
|
|
287
|
+
const pluginEntry = (api.config as any)?.plugins?.entries?.[api.id];
|
|
288
|
+
const allowPromptInjection = pluginEntry?.hooks?.allowPromptInjection !== false;
|
|
289
|
+
if (!allowPromptInjection) {
|
|
290
|
+
api.logger.info("memos-local: allowPromptInjection=false, auto-recall disabled");
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
|
|
294
|
+
}
|
|
295
|
+
|
|
275
296
|
const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
|
|
276
297
|
async (...args: any[]) => {
|
|
277
298
|
const t0 = performance.now();
|
|
@@ -283,6 +304,7 @@ const memosLocalPlugin = {
|
|
|
283
304
|
return result;
|
|
284
305
|
} catch (e) {
|
|
285
306
|
ok = false;
|
|
307
|
+
telemetry.trackError(toolName, (e as Error)?.name ?? "unknown");
|
|
286
308
|
throw e;
|
|
287
309
|
} finally {
|
|
288
310
|
const dur = performance.now() - t0;
|
|
@@ -1097,6 +1119,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1097
1119
|
parameters: Type.Object({}),
|
|
1098
1120
|
execute: trackTool("memory_viewer", async () => {
|
|
1099
1121
|
ctx.log.debug(`memory_viewer called`);
|
|
1122
|
+
telemetry.trackViewerOpened();
|
|
1100
1123
|
const url = `http://127.0.0.1:${viewerPort}`;
|
|
1101
1124
|
return {
|
|
1102
1125
|
content: [
|
|
@@ -1548,6 +1571,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1548
1571
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
1549
1572
|
|
|
1550
1573
|
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
1574
|
+
if (!allowPromptInjection) return {};
|
|
1551
1575
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1552
1576
|
|
|
1553
1577
|
const recallAgentId = hookCtx?.agentId ?? "main";
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "MemOS Local Memory",
|
|
4
4
|
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary, memory_timeline, memory_viewer for layered retrieval.",
|
|
5
5
|
"kind": "memory",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.12",
|
|
7
7
|
"skills": [
|
|
8
8
|
"skill/memos-memory-guide"
|
|
9
9
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "1.0.4-beta.
|
|
3
|
+
"version": "1.0.4-beta.8",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"@huggingface/transformers": "^3.8.0",
|
|
52
52
|
"@sinclair/typebox": "^0.34.48",
|
|
53
53
|
"better-sqlite3": "^12.6.2",
|
|
54
|
-
"posthog-node": "^5.28.0",
|
|
55
54
|
"puppeteer": "^24.38.0",
|
|
56
55
|
"semver": "^7.7.4",
|
|
57
56
|
"uuid": "^10.0.0"
|
package/scripts/postinstall.cjs
CHANGED
|
@@ -112,7 +112,7 @@ try {
|
|
|
112
112
|
function ensureDependencies() {
|
|
113
113
|
phase(0, "检测核心依赖 / Check core dependencies");
|
|
114
114
|
|
|
115
|
-
const coreDeps = ["@sinclair/typebox", "uuid", "
|
|
115
|
+
const coreDeps = ["@sinclair/typebox", "uuid", "@huggingface/transformers"];
|
|
116
116
|
const missing = [];
|
|
117
117
|
for (const dep of coreDeps) {
|
|
118
118
|
try {
|
package/src/capture/index.ts
CHANGED
|
@@ -75,6 +75,7 @@ export function captureMessages(
|
|
|
75
75
|
if (role === "user") {
|
|
76
76
|
content = stripInboundMetadata(content);
|
|
77
77
|
} else {
|
|
78
|
+
content = stripThinkingTags(content);
|
|
78
79
|
content = stripEvidenceWrappers(content, evidenceTag);
|
|
79
80
|
}
|
|
80
81
|
if (!content.trim()) continue;
|
|
@@ -163,6 +164,13 @@ export function stripInboundMetadata(text: string): string {
|
|
|
163
164
|
return stripEnvelopePrefix(result.join("\n")).trim();
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
/** Strip <think…>…</think> blocks emitted by DeepSeek-style reasoning models. */
|
|
168
|
+
const THINKING_TAG_RE = /<think[\s>][\s\S]*?<\/think>\s*/gi;
|
|
169
|
+
|
|
170
|
+
function stripThinkingTags(text: string): string {
|
|
171
|
+
return text.replace(THINKING_TAG_RE, "");
|
|
172
|
+
}
|
|
173
|
+
|
|
166
174
|
function extractEnvelopeTimestamp(text: string): number | null {
|
|
167
175
|
const m = ENVELOPE_EXTRACT_RE.exec(text);
|
|
168
176
|
if (!m) return null;
|
package/src/client/connector.ts
CHANGED
|
@@ -65,7 +65,7 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
65
65
|
try {
|
|
66
66
|
const result = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/join", {
|
|
67
67
|
method: "POST",
|
|
68
|
-
body: JSON.stringify({ teamToken, username: conn.username || "user" }),
|
|
68
|
+
body: JSON.stringify({ teamToken, username: config.sharing?.client?.nickname || conn.username || "user" }),
|
|
69
69
|
}) as any;
|
|
70
70
|
if (result.status === "pending") {
|
|
71
71
|
return {
|
|
@@ -123,13 +123,25 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
123
123
|
|
|
124
124
|
try {
|
|
125
125
|
const me = await hubRequestJson(normalizeHubUrl(hubAddress), userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
126
|
+
const latestUsername = String(me.username ?? "");
|
|
127
|
+
const latestRole = String(me.role ?? "member") as UserRole;
|
|
128
|
+
if (conn && (conn.username !== latestUsername || conn.role !== latestRole)) {
|
|
129
|
+
store.setClientHubConnection({
|
|
130
|
+
hubUrl: conn.hubUrl,
|
|
131
|
+
userId: conn.userId,
|
|
132
|
+
username: latestUsername,
|
|
133
|
+
userToken: conn.userToken,
|
|
134
|
+
role: latestRole,
|
|
135
|
+
connectedAt: conn.connectedAt,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
126
138
|
return {
|
|
127
139
|
connected: true,
|
|
128
140
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
129
141
|
user: {
|
|
130
142
|
id: String(me.id),
|
|
131
|
-
username:
|
|
132
|
-
role:
|
|
143
|
+
username: latestUsername,
|
|
144
|
+
role: latestRole,
|
|
133
145
|
status: String(me.status ?? "active"),
|
|
134
146
|
},
|
|
135
147
|
};
|
|
@@ -151,7 +163,8 @@ export async function autoJoinHub(
|
|
|
151
163
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
152
164
|
const osModule = typeof globalThis.process !== "undefined" ? await import("os") : null;
|
|
153
165
|
const hostname = osModule ? osModule.hostname() : "unknown";
|
|
154
|
-
const
|
|
166
|
+
const nickname = config.sharing?.client?.nickname;
|
|
167
|
+
const username = nickname || (osModule ? osModule.userInfo().username : "user");
|
|
155
168
|
let clientIp = "";
|
|
156
169
|
if (osModule) {
|
|
157
170
|
const nets = osModule.networkInterfaces();
|
package/src/config.ts
CHANGED
|
@@ -75,8 +75,6 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
|
|
|
75
75
|
},
|
|
76
76
|
telemetry: {
|
|
77
77
|
enabled: telemetryEnabled,
|
|
78
|
-
posthogApiKey: cfg.telemetry?.posthogApiKey ?? process.env.POSTHOG_API_KEY ?? "",
|
|
79
|
-
posthogHost: cfg.telemetry?.posthogHost ?? process.env.POSTHOG_HOST ?? "",
|
|
80
78
|
},
|
|
81
79
|
summarizer: (() => {
|
|
82
80
|
const summarizerConfig = resolveProviderFallback<SummarizerConfig>(
|
package/src/hub/server.ts
CHANGED
|
@@ -34,6 +34,11 @@ export class HubServer {
|
|
|
34
34
|
private static readonly RATE_LIMIT_SEARCH = 30;
|
|
35
35
|
private rateBuckets = new Map<string, { count: number; windowStart: number }>();
|
|
36
36
|
|
|
37
|
+
private static readonly OFFLINE_THRESHOLD_MS = 2 * 60 * 1000;
|
|
38
|
+
private static readonly OFFLINE_CHECK_INTERVAL_MS = 30 * 1000;
|
|
39
|
+
private offlineCheckTimer?: ReturnType<typeof setInterval>;
|
|
40
|
+
private knownOnlineUsers = new Set<string>();
|
|
41
|
+
|
|
37
42
|
constructor(private opts: HubServerOptions) {
|
|
38
43
|
this.userManager = new HubUserManager(opts.store, opts.log);
|
|
39
44
|
this.authStatePath = path.join(opts.dataDir, "hub-auth.json");
|
|
@@ -101,10 +106,14 @@ export class HubServer {
|
|
|
101
106
|
this.opts.log.info(`memos-local: bootstrap admin token persisted to ${this.authStatePath}`);
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
this.initOnlineTracking();
|
|
110
|
+
this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
|
|
111
|
+
|
|
104
112
|
return `http://127.0.0.1:${this.port}`;
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
async stop(): Promise<void> {
|
|
116
|
+
if (this.offlineCheckTimer) { clearInterval(this.offlineCheckTimer); this.offlineCheckTimer = undefined; }
|
|
108
117
|
if (!this.server) return;
|
|
109
118
|
const server = this.server;
|
|
110
119
|
this.server = undefined;
|
|
@@ -223,6 +232,7 @@ export class HubServer {
|
|
|
223
232
|
});
|
|
224
233
|
try { this.opts.store.updateHubUserActivity(user.id, joinIp); } catch { /* best-effort */ }
|
|
225
234
|
this.opts.log.info(`Hub: user "${username}" (${user.id}) registered as pending, awaiting admin approval`);
|
|
235
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
226
236
|
return this.json(res, 200, { status: "pending", userId: user.id });
|
|
227
237
|
}
|
|
228
238
|
|
|
@@ -260,6 +270,20 @@ export class HubServer {
|
|
|
260
270
|
return this.json(res, 429, { error: "rate_limit_exceeded", retryAfterMs: HubServer.RATE_WINDOW_MS });
|
|
261
271
|
}
|
|
262
272
|
|
|
273
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/heartbeat") {
|
|
274
|
+
return this.json(res, 200, { ok: true });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
278
|
+
try {
|
|
279
|
+
this.opts.store.updateHubUserActivity(auth.userId, "", 0);
|
|
280
|
+
} catch { /* best-effort */ }
|
|
281
|
+
this.knownOnlineUsers.delete(auth.userId);
|
|
282
|
+
this.notifyAdmins("user_offline", "user", auth.username, auth.userId);
|
|
283
|
+
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily`);
|
|
284
|
+
return this.json(res, 200, { ok: true });
|
|
285
|
+
}
|
|
286
|
+
|
|
263
287
|
if (req.method === "GET" && routePath === "/api/v1/hub/me") {
|
|
264
288
|
const user = this.opts.store.getHubUser(auth.userId);
|
|
265
289
|
if (!user) return this.json(res, 401, { error: "unauthorized" });
|
|
@@ -300,6 +324,7 @@ export class HubServer {
|
|
|
300
324
|
const token = issueUserToken({ userId: String(body.userId), username: String(body.username || ""), role: "member", status: "active" }, this.authSecret);
|
|
301
325
|
const approved = this.userManager.approveUser(String(body.userId), token);
|
|
302
326
|
if (!approved) return this.json(res, 404, { error: "not_found" });
|
|
327
|
+
try { this.opts.store.updateHubUserActivity(String(body.userId), ""); } catch { /* best-effort */ }
|
|
303
328
|
return this.json(res, 200, { status: "active", token });
|
|
304
329
|
}
|
|
305
330
|
|
|
@@ -315,12 +340,16 @@ export class HubServer {
|
|
|
315
340
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
316
341
|
const users = this.opts.store.listHubUsers().filter(u => u.status === "active");
|
|
317
342
|
const contribs = this.opts.store.getHubUserContributions();
|
|
343
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
344
|
+
const now = Date.now();
|
|
318
345
|
return this.json(res, 200, { users: users.map(u => {
|
|
319
346
|
const c = contribs[u.id] || { memoryCount: 0, taskCount: 0, skillCount: 0 };
|
|
347
|
+
const isOnline = u.id === ownerId || (!!u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS);
|
|
320
348
|
return {
|
|
321
349
|
id: u.id, username: u.username, role: u.role, status: u.status,
|
|
322
350
|
deviceName: u.deviceName, createdAt: u.createdAt, approvedAt: u.approvedAt,
|
|
323
351
|
lastIp: u.lastIp || "", lastActiveAt: u.lastActiveAt,
|
|
352
|
+
isOwner: u.id === ownerId, isOnline,
|
|
324
353
|
memoryCount: c.memoryCount, taskCount: c.taskCount, skillCount: c.skillCount,
|
|
325
354
|
};
|
|
326
355
|
}) });
|
|
@@ -332,6 +361,9 @@ export class HubServer {
|
|
|
332
361
|
const userId = String(body?.userId || "");
|
|
333
362
|
const newRole = String(body?.role || "");
|
|
334
363
|
if (!userId || (newRole !== "admin" && newRole !== "member")) return this.json(res, 400, { error: "invalid_params" });
|
|
364
|
+
if (newRole === "member" && userId === this.authState.bootstrapAdminUserId) {
|
|
365
|
+
return this.json(res, 403, { error: "cannot_demote_owner", message: "The hub owner cannot be demoted" });
|
|
366
|
+
}
|
|
335
367
|
const user = this.opts.store.getHubUser(userId);
|
|
336
368
|
if (!user || user.status !== "active") return this.json(res, 404, { error: "not_found" });
|
|
337
369
|
const updatedUser = { ...user, role: newRole as "admin" | "member" };
|
|
@@ -353,8 +385,16 @@ export class HubServer {
|
|
|
353
385
|
}
|
|
354
386
|
const user = this.opts.store.getHubUser(userId);
|
|
355
387
|
if (!user || user.status !== "active") return this.json(res, 404, { error: "not_found" });
|
|
356
|
-
const
|
|
357
|
-
|
|
388
|
+
const ttlMs = user.role === "admin" ? 3650 * 24 * 60 * 60 * 1000 : undefined;
|
|
389
|
+
const newToken = issueUserToken(
|
|
390
|
+
{ userId: user.id, username: newUsername, role: user.role, status: user.status },
|
|
391
|
+
this.authSecret,
|
|
392
|
+
ttlMs,
|
|
393
|
+
);
|
|
394
|
+
this.userManager.approveUser(user.id, newToken);
|
|
395
|
+
const updated = this.opts.store.getHubUser(userId)!;
|
|
396
|
+
const finalUser = { ...updated, username: newUsername };
|
|
397
|
+
this.opts.store.upsertHubUser(finalUser);
|
|
358
398
|
this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
|
|
359
399
|
return this.json(res, 200, { ok: true, username: newUsername });
|
|
360
400
|
}
|
|
@@ -365,6 +405,7 @@ export class HubServer {
|
|
|
365
405
|
const userId = String(body?.userId || "");
|
|
366
406
|
if (!userId) return this.json(res, 400, { error: "missing_user_id" });
|
|
367
407
|
if (userId === auth.userId) return this.json(res, 400, { error: "cannot_remove_self" });
|
|
408
|
+
if (userId === this.authState.bootstrapAdminUserId) return this.json(res, 403, { error: "cannot_remove_owner", message: "The hub owner cannot be removed" });
|
|
368
409
|
const cleanResources = body?.cleanResources === true;
|
|
369
410
|
const deleted = this.opts.store.deleteHubUser(userId, cleanResources);
|
|
370
411
|
if (!deleted) return this.json(res, 404, { error: "not_found" });
|
|
@@ -376,6 +417,7 @@ export class HubServer {
|
|
|
376
417
|
const body = await this.readJson(req);
|
|
377
418
|
if (!body?.task) return this.json(res, 400, { error: "invalid_payload" });
|
|
378
419
|
const task = { ...body.task, sourceUserId: auth.userId };
|
|
420
|
+
const existingTask = task.sourceTaskId ? this.opts.store.getHubTaskBySource(auth.userId, task.sourceTaskId) : null;
|
|
379
421
|
this.opts.store.upsertHubTask(task);
|
|
380
422
|
const chunks = Array.isArray(body.chunks) ? body.chunks : [];
|
|
381
423
|
const chunkIds: string[] = [];
|
|
@@ -383,16 +425,23 @@ export class HubServer {
|
|
|
383
425
|
this.opts.store.upsertHubChunk({ ...chunk, sourceUserId: auth.userId });
|
|
384
426
|
chunkIds.push(chunk.id);
|
|
385
427
|
}
|
|
386
|
-
// Async embedding: don't block the response
|
|
387
428
|
if (this.opts.embedder && chunkIds.length > 0) {
|
|
388
429
|
this.embedChunksAsync(chunkIds, chunks);
|
|
389
430
|
}
|
|
431
|
+
if (!existingTask) {
|
|
432
|
+
this.notifyAdmins("resource_shared", "task", String(task.title || task.sourceTaskId || ""), auth.userId);
|
|
433
|
+
}
|
|
390
434
|
return this.json(res, 200, { ok: true, chunks: chunkIds.length });
|
|
391
435
|
}
|
|
392
436
|
|
|
393
437
|
if (req.method === "POST" && routePath === "/api/v1/hub/tasks/unshare") {
|
|
394
438
|
const body = await this.readJson(req);
|
|
395
|
-
|
|
439
|
+
const srcTaskId = String(body.sourceTaskId);
|
|
440
|
+
const existing = this.opts.store.getHubTaskBySource(auth.userId, srcTaskId);
|
|
441
|
+
this.opts.store.deleteHubTaskBySource(auth.userId, srcTaskId);
|
|
442
|
+
if (existing) {
|
|
443
|
+
this.notifyAdmins("resource_unshared", "task", existing.title || srcTaskId, auth.userId);
|
|
444
|
+
}
|
|
396
445
|
return this.json(res, 200, { ok: true });
|
|
397
446
|
}
|
|
398
447
|
|
|
@@ -423,6 +472,9 @@ export class HubServer {
|
|
|
423
472
|
if (this.opts.embedder) {
|
|
424
473
|
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
425
474
|
}
|
|
475
|
+
if (!existing) {
|
|
476
|
+
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
477
|
+
}
|
|
426
478
|
return this.json(res, 200, { ok: true, memoryId, visibility });
|
|
427
479
|
}
|
|
428
480
|
|
|
@@ -430,7 +482,11 @@ export class HubServer {
|
|
|
430
482
|
const body = await this.readJson(req);
|
|
431
483
|
const sourceChunkId = String(body?.sourceChunkId || "");
|
|
432
484
|
if (!sourceChunkId) return this.json(res, 400, { error: "missing_source_chunk_id" });
|
|
485
|
+
const existing = this.opts.store.getHubMemoryBySource(auth.userId, sourceChunkId);
|
|
433
486
|
this.opts.store.deleteHubMemoryBySource(auth.userId, sourceChunkId);
|
|
487
|
+
if (existing) {
|
|
488
|
+
this.notifyAdmins("resource_unshared", "memory", existing.summary || existing.content?.slice(0, 60) || sourceChunkId, auth.userId);
|
|
489
|
+
}
|
|
434
490
|
return this.json(res, 200, { ok: true });
|
|
435
491
|
}
|
|
436
492
|
|
|
@@ -580,6 +636,9 @@ export class HubServer {
|
|
|
580
636
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
581
637
|
updatedAt: Date.now(),
|
|
582
638
|
});
|
|
639
|
+
if (!existing) {
|
|
640
|
+
this.notifyAdmins("resource_shared", "skill", String(metadata.name || sourceSkillId), auth.userId);
|
|
641
|
+
}
|
|
583
642
|
return this.json(res, 200, { ok: true, skillId, visibility });
|
|
584
643
|
}
|
|
585
644
|
|
|
@@ -602,7 +661,12 @@ export class HubServer {
|
|
|
602
661
|
|
|
603
662
|
if (req.method === "POST" && routePath === "/api/v1/hub/skills/unpublish") {
|
|
604
663
|
const body = await this.readJson(req);
|
|
605
|
-
|
|
664
|
+
const srcSkillId = String(body?.sourceSkillId || "");
|
|
665
|
+
const existing = this.opts.store.getHubSkillBySource(auth.userId, srcSkillId);
|
|
666
|
+
this.opts.store.deleteHubSkillBySource(auth.userId, srcSkillId);
|
|
667
|
+
if (existing) {
|
|
668
|
+
this.notifyAdmins("resource_unshared", "skill", existing.name || srcSkillId, auth.userId);
|
|
669
|
+
}
|
|
606
670
|
return this.json(res, 200, { ok: true });
|
|
607
671
|
}
|
|
608
672
|
|
|
@@ -614,6 +678,38 @@ export class HubServer {
|
|
|
614
678
|
return this.json(res, 200, { tasks });
|
|
615
679
|
}
|
|
616
680
|
|
|
681
|
+
const hubTaskDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-tasks\/([^/]+)\/detail$/) : null;
|
|
682
|
+
if (hubTaskDetailMatch) {
|
|
683
|
+
const taskId = decodeURIComponent(hubTaskDetailMatch[1]);
|
|
684
|
+
const task = this.opts.store.getHubTaskById(taskId);
|
|
685
|
+
if (!task) return this.json(res, 404, { error: "not_found" });
|
|
686
|
+
const chunks = this.opts.store.listHubChunksByTaskId(taskId);
|
|
687
|
+
return this.json(res, 200, {
|
|
688
|
+
id: task.id, title: task.title, summary: task.summary,
|
|
689
|
+
startedAt: task.createdAt, endedAt: task.updatedAt,
|
|
690
|
+
chunks: chunks.map(c => ({ role: c.role, content: c.content, summary: c.summary, kind: c.kind, createdAt: c.createdAt })),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const hubSkillDetailMatch = req.method === "GET" ? routePath.match(/^\/api\/v1\/hub\/shared-skills\/([^/]+)\/detail$/) : null;
|
|
695
|
+
if (hubSkillDetailMatch) {
|
|
696
|
+
const skillId = decodeURIComponent(hubSkillDetailMatch[1]);
|
|
697
|
+
const skill = this.opts.store.getHubSkillById(skillId);
|
|
698
|
+
if (!skill) return this.json(res, 404, { error: "not_found" });
|
|
699
|
+
let files: Array<{ path: string; type: string; size: number }> = [];
|
|
700
|
+
try {
|
|
701
|
+
const bundle = JSON.parse(skill.bundle || "{}");
|
|
702
|
+
if (Array.isArray(bundle.files)) {
|
|
703
|
+
files = bundle.files.map((f: any) => ({ path: f.path ?? f.name ?? "unknown", type: f.type ?? "file", size: f.size ?? (f.content ? f.content.length : 0) }));
|
|
704
|
+
}
|
|
705
|
+
} catch { /* ignore parse error */ }
|
|
706
|
+
return this.json(res, 200, {
|
|
707
|
+
skill: { id: skill.id, name: skill.name, description: skill.description, version: skill.version, qualityScore: skill.qualityScore, status: "published" },
|
|
708
|
+
files,
|
|
709
|
+
versions: [],
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
617
713
|
const adminTaskDeleteMatch = req.method === "DELETE" ? routePath.match(/^\/api\/v1\/hub\/admin\/shared-tasks\/([^/]+)$/) : null;
|
|
618
714
|
if (adminTaskDeleteMatch) {
|
|
619
715
|
if (auth.role !== "admin") return this.json(res, 403, { error: "forbidden" });
|
|
@@ -706,6 +802,62 @@ export class HubServer {
|
|
|
706
802
|
return this.json(res, 404, { error: "not_found" });
|
|
707
803
|
}
|
|
708
804
|
|
|
805
|
+
private notifyAdmins(type: string, resource: string, title: string, fromUserId: string): void {
|
|
806
|
+
try {
|
|
807
|
+
const admins = this.opts.store.listHubUsers("active").filter(u => u.role === "admin" && u.id !== fromUserId);
|
|
808
|
+
for (const admin of admins) {
|
|
809
|
+
this.opts.store.insertHubNotification({ id: randomUUID(), userId: admin.id, type, resource, title });
|
|
810
|
+
}
|
|
811
|
+
} catch { /* best-effort */ }
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private initOnlineTracking(): void {
|
|
815
|
+
try {
|
|
816
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
817
|
+
const users = this.opts.store.listHubUsers("active");
|
|
818
|
+
const now = Date.now();
|
|
819
|
+
for (const u of users) {
|
|
820
|
+
if (u.id === ownerId) continue;
|
|
821
|
+
if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
|
|
822
|
+
this.knownOnlineUsers.add(u.id);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} catch { /* best-effort */ }
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private checkOfflineUsers(): void {
|
|
829
|
+
try {
|
|
830
|
+
const ownerId = this.authState.bootstrapAdminUserId || "";
|
|
831
|
+
const users = this.opts.store.listHubUsers("active");
|
|
832
|
+
const now = Date.now();
|
|
833
|
+
const currentlyOnline = new Set<string>();
|
|
834
|
+
for (const u of users) {
|
|
835
|
+
if (u.id === ownerId) continue;
|
|
836
|
+
if (u.lastActiveAt && now - u.lastActiveAt < HubServer.OFFLINE_THRESHOLD_MS) {
|
|
837
|
+
currentlyOnline.add(u.id);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
for (const uid of this.knownOnlineUsers) {
|
|
841
|
+
if (!currentlyOnline.has(uid)) {
|
|
842
|
+
const user = users.find(u => u.id === uid);
|
|
843
|
+
if (user) {
|
|
844
|
+
this.notifyAdmins("user_offline", "user", user.username, uid);
|
|
845
|
+
this.opts.log.info(`Hub: user "${user.username}" (${uid}) went offline`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (const uid of currentlyOnline) {
|
|
850
|
+
if (!this.knownOnlineUsers.has(uid)) {
|
|
851
|
+
const user = users.find(u => u.id === uid);
|
|
852
|
+
if (user) {
|
|
853
|
+
this.notifyAdmins("user_online", "user", user.username, uid);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
this.knownOnlineUsers = currentlyOnline;
|
|
858
|
+
} catch { /* best-effort */ }
|
|
859
|
+
}
|
|
860
|
+
|
|
709
861
|
private authenticate(req: http.IncomingMessage) {
|
|
710
862
|
const header = req.headers.authorization;
|
|
711
863
|
if (!header || !header.startsWith("Bearer ")) return null;
|
|
@@ -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,
|