openclaw-memory-alibaba-local 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,7 +42,7 @@ OpenClaw 记忆插件:**本地 LanceDB** 向量存储。支持用户记忆三
42
42
  }
43
43
  ```
44
44
 
45
- 可选 **`local`** 字段:`commandPrefix`(默认使用 `llama-embedding -m ~/.openclaw/embedding_model/embeddinggemma-300M-Q8_0.gguf -f /dev/stdin --embd-output-format json`)、`dimensions`(默认 **768**)、`maxToken`(默认 **2048**)。命令需从 **stdin** 读入待嵌入文本;推荐使用 **`--embd-output-format json`**(与 OpenAI 列表格式兼容)。
45
+ 可选 **`local`** 字段:`commandPrefix`(默认使用 `llama-embedding -m ~/.openclaw/embedding_model/embeddinggemma-300M-Q8_0.gguf --embd-output-format json`)、`dimensions`(默认 **768**)、`maxToken`(默认 **2048**)。命令需从 **stdin** 读入待嵌入文本;推荐使用 **`--embd-output-format json`**(与 OpenAI 列表格式兼容)。
46
46
 
47
47
  ## 远程 embedding(OpenAI 兼容)
48
48
 
@@ -13,7 +13,7 @@ export type EmbeddingBackend = {
13
13
  };
14
14
 
15
15
  const DEFAULT_LOCAL_PREFIX =
16
- "llama-embedding -m ~/.openclaw/embedding_model/embeddinggemma-300M-Q8_0.gguf -f /dev/stdin --embd-output-format json ";
16
+ "llama-embedding -m ~/.openclaw/embedding_model/embeddinggemma-300M-Q8_0.gguf --embd-output-format json ";
17
17
 
18
18
  function expandTildeInCommandPrefix(prefix: string): string {
19
19
  const home = process.env.HOME || "";
package/index.ts CHANGED
@@ -53,7 +53,7 @@ import {
53
53
  saveAgentEndCursorMap,
54
54
  } from "./capture-state.js";
55
55
  import { LANCEDB_TABLE_NAME, MemoryDB } from "./db.js";
56
- import { registerMemoryPanelRoutes } from "./web/memory-routes.js";
56
+ import { registerMemoryAdminGatewayMethods, registerMemoryPanelRoutes } from "./web/memory-routes.js";
57
57
  import type { MemoryEntry, MemorySearchResult } from "./db.js";
58
58
  import {
59
59
  buildUserMemoryExtractionPrompt,
@@ -1241,23 +1241,25 @@ const memoryPlugin = {
1241
1241
  const getDbAndBackend = (): { db: MemoryDB; backend: EmbeddingBackend } | null =>
1242
1242
  backend ? { db, backend } : null;
1243
1243
 
1244
+ const memoryAdminOpts = backend
1245
+ ? {
1246
+ encodeForStorage: (text: string) => backend!.encodeForStorage(text),
1247
+ vectorDim: db.getEmbeddingVectorDim(),
1248
+ }
1249
+ : {
1250
+ vectorDim: db.getEmbeddingVectorDim(),
1251
+ };
1252
+
1253
+ if (typeof api.registerGatewayMethod === "function") {
1254
+ registerMemoryAdminGatewayMethods(api.registerGatewayMethod.bind(api), db, cfg, api.logger, memoryAdminOpts);
1255
+ } else {
1256
+ api.logger.warn("openclaw-memory-alibaba-local: registerGatewayMethod missing — memory admin data API disabled");
1257
+ }
1258
+
1244
1259
  if (typeof api.registerHttpRoute === "function") {
1245
- registerMemoryPanelRoutes(
1246
- api.registerHttpRoute.bind(api),
1247
- db,
1248
- cfg,
1249
- api.logger,
1250
- backend
1251
- ? {
1252
- encodeForStorage: (text) => backend!.encodeForStorage(text),
1253
- vectorDim: db.getEmbeddingVectorDim(),
1254
- }
1255
- : {
1256
- vectorDim: db.getEmbeddingVectorDim(),
1257
- },
1258
- );
1260
+ registerMemoryPanelRoutes(api.registerHttpRoute.bind(api), db, cfg, api.logger, memoryAdminOpts);
1259
1261
  } else {
1260
- api.logger.warn("openclaw-memory-alibaba-local: registerHttpRoute missing — /plugins/memory UI disabled");
1262
+ api.logger.warn("openclaw-memory-alibaba-local: registerHttpRoute missing — /plugins/memory HTML shell disabled");
1261
1263
  }
1262
1264
 
1263
1265
  // --- Tools: memory_recall, memory_store, memory_forget ---
@@ -19,7 +19,7 @@
19
19
  "mode": { "type": "string", "enum": ["local", "remote"], "default": "local" },
20
20
  "commandPrefix": {
21
21
  "type": "string",
22
- "description": "Shell command; stdin = text to embed. Default uses llama-embedding + -f /dev/stdin --embd-output-format json."
22
+ "description": "Shell command; stdin = text to embed. Default uses llama-embedding + --embd-output-format json."
23
23
  },
24
24
  "dimensions": { "type": "number", "description": "768 default (local); 1024 default (remote) when omitted." },
25
25
  "maxToken": { "type": "number", "description": "2048 default (remote) when omitted." },
@@ -136,7 +136,7 @@
136
136
  },
137
137
  "embedding.commandPrefix": {
138
138
  "label": "Local embed command",
139
- "placeholder": "llama-embedding -m ~/.openclaw/... -f /dev/stdin --embd-output-format json",
139
+ "placeholder": "llama-embedding -m ~/.openclaw/... --embd-output-format json",
140
140
  "help": "Must read prompt from stdin; JSON output recommended for parsing."
141
141
  },
142
142
  "embedding.apiKey": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-memory-alibaba-local",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OpenClaw memory plugin: local LanceDB + DashScope-compatible embeddings",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "embedding-backend.ts",
28
28
  "categories.ts",
29
29
  "prompts.ts",
30
+ "web/memory-admin-ops.ts",
30
31
  "web/memory-routes.ts",
31
32
  "web/memory-ui.ts",
32
33
  "openclaw.plugin.json",
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Shared admin-panel operations for memory DB (used by Gateway WebSocket RPC).
3
+ */
4
+
5
+ import type { MemoryCategory, MemoryConfig } from "../config.js";
6
+ import {
7
+ FULL_CONTEXT_ASSISTANT,
8
+ FULL_CONTEXT_MEMORY,
9
+ FULL_CONTEXT_OTHERS,
10
+ FULL_CONTEXT_SOURCE_CATEGORIES,
11
+ FULL_CONTEXT_SYSTEM,
12
+ FULL_CONTEXT_TOOL,
13
+ FULL_CONTEXT_TOOL_RESULT,
14
+ FULL_CONTEXT_USER,
15
+ MANUAL_INSERT_SESSION,
16
+ MEMORY_CATEGORY_LABEL_ZH,
17
+ SELF_IMPROVING_CATEGORIES,
18
+ USER_MEMORY_CATEGORIES,
19
+ isFullContextSourceCategory,
20
+ } from "../categories.js";
21
+ import type { AdminListFilters, MemoryDB } from "../db.js";
22
+
23
+ const MANUAL_ADD_MAX_CHARS = 8000;
24
+
25
+ export type MemoryAdminOpsOpts = {
26
+ encodeForStorage?: (text: string) => Promise<{ chunks: string[]; vectors: number[][] }>;
27
+ vectorDim?: number;
28
+ };
29
+
30
+ export type MemoryAdminOpsContext = {
31
+ db: MemoryDB;
32
+ cfg: MemoryConfig;
33
+ opts?: MemoryAdminOpsOpts | null;
34
+ };
35
+
36
+ export type AdminOpResult<T = unknown> =
37
+ | { ok: true; data: T }
38
+ | { ok: false; status: number; body: Record<string, unknown> };
39
+
40
+ function categoryUsesRealEmbedding(category: MemoryCategory): boolean {
41
+ return category !== FULL_CONTEXT_MEMORY && !isFullContextSourceCategory(category);
42
+ }
43
+
44
+ function writableCategoriesForPanel(cfg: MemoryConfig): MemoryCategory[] {
45
+ const out: MemoryCategory[] = [...USER_MEMORY_CATEGORIES];
46
+ if (cfg.enableFullContextMemory) {
47
+ out.push(
48
+ FULL_CONTEXT_USER,
49
+ FULL_CONTEXT_ASSISTANT,
50
+ FULL_CONTEXT_SYSTEM,
51
+ FULL_CONTEXT_TOOL,
52
+ FULL_CONTEXT_TOOL_RESULT,
53
+ FULL_CONTEXT_OTHERS,
54
+ );
55
+ }
56
+ if (cfg.enableSelfImprovingMemory) {
57
+ out.push(...SELF_IMPROVING_CATEGORIES);
58
+ }
59
+ return out;
60
+ }
61
+
62
+ export function buildPanelConfigPayload(cfg: MemoryConfig) {
63
+ return {
64
+ enableFullContextMemory: cfg.enableFullContextMemory,
65
+ enableSelfImprovingMemory: cfg.enableSelfImprovingMemory,
66
+ categoryLabelsZh: { ...MEMORY_CATEGORY_LABEL_ZH },
67
+ tabCategories: {
68
+ user: [...USER_MEMORY_CATEGORIES],
69
+ self: cfg.enableSelfImprovingMemory ? [...SELF_IMPROVING_CATEGORIES] : [],
70
+ full: cfg.enableFullContextMemory ? [...FULL_CONTEXT_SOURCE_CATEGORIES] : [],
71
+ },
72
+ memoryTypeFilterOptions: cfg.adminPanelMemoryTypeOptions,
73
+ };
74
+ }
75
+
76
+ function tabToCategories(tab: string, cfg: MemoryConfig): MemoryCategory[] {
77
+ if (tab === "full") {
78
+ return cfg.enableFullContextMemory ? [...FULL_CONTEXT_SOURCE_CATEGORIES] : [];
79
+ }
80
+ if (tab === "self") {
81
+ return cfg.enableSelfImprovingMemory ? [...SELF_IMPROVING_CATEGORIES] : [];
82
+ }
83
+ return [...USER_MEMORY_CATEGORIES];
84
+ }
85
+
86
+ function parseOptionalTimeMs(iso: string | null | undefined): number | undefined {
87
+ if (!iso) {
88
+ return undefined;
89
+ }
90
+ const t = Date.parse(String(iso));
91
+ if (Number.isNaN(t)) {
92
+ return undefined;
93
+ }
94
+ return t;
95
+ }
96
+
97
+ async function ensureDb(ctx: MemoryAdminOpsContext): Promise<AdminOpResult<null>> {
98
+ try {
99
+ await ctx.db.ensureReady();
100
+ return { ok: true, data: null };
101
+ } catch (e) {
102
+ return { ok: false, status: 503, body: { error: "Database unavailable", detail: String(e) } };
103
+ }
104
+ }
105
+
106
+ export async function opMemoryAdminConfig(ctx: MemoryAdminOpsContext): Promise<AdminOpResult<unknown>> {
107
+ const r = await ensureDb(ctx);
108
+ if (!r.ok) {
109
+ return r;
110
+ }
111
+ return { ok: true, data: buildPanelConfigPayload(ctx.cfg) };
112
+ }
113
+
114
+ export async function opMemoryAdminFacets(ctx: MemoryAdminOpsContext): Promise<AdminOpResult<unknown>> {
115
+ const r = await ensureDb(ctx);
116
+ if (!r.ok) {
117
+ return r;
118
+ }
119
+ try {
120
+ const facets = await ctx.db.listAdminFacets([], undefined, undefined);
121
+ return { ok: true, data: facets };
122
+ } catch (e) {
123
+ return { ok: false, status: 400, body: { error: String(e) } };
124
+ }
125
+ }
126
+
127
+ export async function opMemoryAdminDashboard(
128
+ ctx: MemoryAdminOpsContext,
129
+ params: Record<string, unknown>,
130
+ ): Promise<AdminOpResult<unknown>> {
131
+ const r = await ensureDb(ctx);
132
+ if (!r.ok) {
133
+ return r;
134
+ }
135
+ const timeFromMs = parseOptionalTimeMs(params.timeFrom as string | undefined);
136
+ const timeToMs = parseOptionalTimeMs(params.timeTo as string | undefined);
137
+ if (timeFromMs === undefined || timeToMs === undefined) {
138
+ return { ok: false, status: 400, body: { error: "timeFrom and timeTo are required (ISO 8601)" } };
139
+ }
140
+ const agentId = String(params.agentId ?? "").trim();
141
+ const sessionId = String(params.sessionId ?? "").trim();
142
+ if (!agentId) {
143
+ return { ok: false, status: 400, body: { error: "缺少 agentId:请先选择 Agent" } };
144
+ }
145
+ try {
146
+ const agg = await ctx.db.getAdminDashboardAggregates(timeFromMs, timeToMs, agentId, sessionId);
147
+ return { ok: true, data: agg };
148
+ } catch (e) {
149
+ return { ok: false, status: 400, body: { error: String(e) } };
150
+ }
151
+ }
152
+
153
+ export async function opMemoryAdminList(
154
+ ctx: MemoryAdminOpsContext,
155
+ params: Record<string, unknown>,
156
+ ): Promise<AdminOpResult<unknown>> {
157
+ const r = await ensureDb(ctx);
158
+ if (!r.ok) {
159
+ return r;
160
+ }
161
+ const cfg = ctx.cfg;
162
+ const tab = String(params.tab || "user");
163
+ const agentId = String(params.agentId ?? "").trim();
164
+ const sessionId = String(params.sessionId ?? "").trim();
165
+ if (!agentId) {
166
+ return { ok: false, status: 400, body: { error: "缺少 agentId:请先选择 Agent" } };
167
+ }
168
+ const baseCats = tabToCategories(tab, cfg);
169
+ if (baseCats.length === 0) {
170
+ return { ok: true, data: { items: [], total: 0, page: 1, pageSize: 100 } };
171
+ }
172
+ const timeFromMs = parseOptionalTimeMs(params.timeFrom as string | undefined);
173
+ const timeToMs = parseOptionalTimeMs(params.timeTo as string | undefined);
174
+ const page = Math.max(1, parseInt(String(params.page || "1"), 10) || 1);
175
+ const pageSize = Math.min(500, Math.max(1, parseInt(String(params.limit || "100"), 10) || 100));
176
+
177
+ const categoryOne = String(params.category ?? "").trim();
178
+ let filterCats = baseCats;
179
+ if (categoryOne) {
180
+ if (!baseCats.includes(categoryOne as MemoryCategory)) {
181
+ return { ok: false, status: 400, body: { error: "category 与当前 Tab 不匹配" } };
182
+ }
183
+ const listTab = tab === "full" ? "full" : tab === "self" ? "self" : "user";
184
+ const optList = cfg.adminPanelMemoryTypeOptions[listTab];
185
+ if (!optList.some((o) => o.category === categoryOne)) {
186
+ return { ok: false, status: 400, body: { error: "category 不在插件配置的记忆类型筛选项中" } };
187
+ }
188
+ filterCats = [categoryOne as MemoryCategory];
189
+ }
190
+
191
+ const filters: AdminListFilters = {
192
+ categories: filterCats,
193
+ timeFromMs,
194
+ timeToMs,
195
+ agentId,
196
+ };
197
+ if (sessionId) {
198
+ filters.sessionId = sessionId;
199
+ }
200
+
201
+ try {
202
+ const sortDesc = params.sortDesc !== false && String(params.sortDesc) !== "false";
203
+ const adminTab = tab === "full" ? "full" : tab === "self" ? "self" : "user";
204
+ const { total, items } = await ctx.db.listAdminFiltered(filters, page, pageSize, {
205
+ adminTab,
206
+ sortDesc,
207
+ });
208
+ return { ok: true, data: { items, total, page, pageSize } };
209
+ } catch (e) {
210
+ return { ok: false, status: 400, body: { error: String(e) } };
211
+ }
212
+ }
213
+
214
+ export async function opMemoryAdminDelete(
215
+ ctx: MemoryAdminOpsContext,
216
+ params: Record<string, unknown>,
217
+ ): Promise<AdminOpResult<unknown>> {
218
+ const r = await ensureDb(ctx);
219
+ if (!r.ok) {
220
+ return r;
221
+ }
222
+ const itemsRaw = params.items;
223
+ const items = Array.isArray(itemsRaw) ? itemsRaw : [];
224
+ const normalized: { agentId: string; id: string }[] = [];
225
+ for (const it of items) {
226
+ const o = it as { agentId?: string; id?: string };
227
+ if (o?.agentId && o?.id) {
228
+ normalized.push({ agentId: String(o.agentId), id: String(o.id) });
229
+ }
230
+ }
231
+ if (normalized.length === 0) {
232
+ return { ok: false, status: 400, body: { error: "items required" } };
233
+ }
234
+ const n = await ctx.db.deleteMany(normalized);
235
+ return { ok: true, data: { deleted: n } };
236
+ }
237
+
238
+ export async function opMemoryAdminAdd(
239
+ ctx: MemoryAdminOpsContext,
240
+ params: Record<string, unknown>,
241
+ ): Promise<AdminOpResult<unknown>> {
242
+ const r = await ensureDb(ctx);
243
+ if (!r.ok) {
244
+ return r;
245
+ }
246
+ const enc = ctx.opts?.encodeForStorage;
247
+ const vectorDim =
248
+ typeof ctx.opts?.vectorDim === "number" && ctx.opts.vectorDim > 0 ? ctx.opts.vectorDim : 768;
249
+ const agentId = String(params.agentId ?? "").trim();
250
+ const textRaw = params.text == null ? "" : String(params.text);
251
+ const category = String(params.category ?? "").trim() as MemoryCategory;
252
+ if (!agentId) {
253
+ return { ok: false, status: 400, body: { error: "agentId required" } };
254
+ }
255
+ const text = textRaw.trim();
256
+ if (!text.length) {
257
+ return { ok: false, status: 400, body: { error: "text required" } };
258
+ }
259
+ const allowed = new Set(writableCategoriesForPanel(ctx.cfg));
260
+ if (!allowed.has(category)) {
261
+ return { ok: false, status: 400, body: { error: "invalid or disabled category" } };
262
+ }
263
+ const textForEmbed = text.length > MANUAL_ADD_MAX_CHARS ? text.slice(0, MANUAL_ADD_MAX_CHARS) : text;
264
+ const needsRealEmbed = categoryUsesRealEmbedding(category);
265
+ if (needsRealEmbed && !enc) {
266
+ return { ok: false, status: 503, body: { error: "Embedding not configured; plugin needs embedding in config." } };
267
+ }
268
+ let vectors: number[][];
269
+ if (needsRealEmbed) {
270
+ try {
271
+ const out = await enc!(textForEmbed);
272
+ vectors = out.vectors;
273
+ } catch (e) {
274
+ return { ok: false, status: 502, body: { error: `embed failed: ${String(e)}` } };
275
+ }
276
+ if (vectors.length === 0) {
277
+ return { ok: false, status: 400, body: { error: "nothing to embed (empty after chunking)" } };
278
+ }
279
+ } else {
280
+ vectors = [Array.from({ length: vectorDim }, () => 0)];
281
+ }
282
+ const stored = await ctx.db.storeMany(
283
+ agentId,
284
+ vectors.map((vector, idx) => ({
285
+ text: textForEmbed,
286
+ vector,
287
+ importance: 1,
288
+ category,
289
+ userId: "",
290
+ sessionId: MANUAL_INSERT_SESSION,
291
+ seqInBatch: 0,
292
+ chunkIndex: idx,
293
+ })),
294
+ );
295
+ return {
296
+ ok: true,
297
+ data: {
298
+ id: stored[0]!.id,
299
+ createdAt: stored[0]!.createdAt,
300
+ chunkRows: stored.length,
301
+ },
302
+ };
303
+ }
@@ -1,77 +1,28 @@
1
1
  /**
2
- * Memory admin panel — /plugins/memory (HTML + JSON API).
3
- * Auth aligned with openclaw-observability: gateway.auth.token ?token= or Bearer.
2
+ * Memory admin panel — /plugins/memory (HTML shell) + Gateway WebSocket RPC (memory.admin.*).
3
+ * Auth aligned with OpenClaw gateway: connect with gateway token + operator scopes (same as Control UI).
4
4
  */
5
5
 
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import type { IncomingMessage, ServerResponse } from "node:http";
10
- import type { MemoryCategory, MemoryConfig } from "../config.js";
11
- import {
12
- FULL_CONTEXT_ASSISTANT,
13
- FULL_CONTEXT_MEMORY,
14
- FULL_CONTEXT_OTHERS,
15
- FULL_CONTEXT_SOURCE_CATEGORIES,
16
- FULL_CONTEXT_SYSTEM,
17
- FULL_CONTEXT_TOOL,
18
- FULL_CONTEXT_TOOL_RESULT,
19
- FULL_CONTEXT_USER,
20
- MANUAL_INSERT_SESSION,
21
- MEMORY_CATEGORY_LABEL_ZH,
22
- SELF_IMPROVING_CATEGORIES,
23
- USER_MEMORY_CATEGORIES,
24
- isFullContextSourceCategory,
25
- } from "../categories.js";
10
+ import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
11
+ import type { MemoryConfig } from "../config.js";
26
12
  import type { MemoryDB } from "../db.js";
27
- import type { AdminListFilters } from "../db.js";
28
13
  import { getMemoryPanelHtml } from "./memory-ui.js";
29
-
30
- const MANUAL_ADD_MAX_CHARS = 8000;
31
-
32
- export type MemoryPanelRoutesOpts = {
33
- /** Required for POST /api/add when category needs real embeddings (user / self-improving). */
34
- encodeForStorage?: (text: string) => Promise<{ chunks: string[]; vectors: number[][] }>;
35
- /** LanceDB vector width; used for full_context_* manual add (zero placeholder, no embed). */
36
- vectorDim?: number;
37
- };
38
-
39
- function categoryUsesRealEmbedding(category: MemoryCategory): boolean {
40
- return category !== FULL_CONTEXT_MEMORY && !isFullContextSourceCategory(category);
41
- }
42
-
43
- function writableCategoriesForPanel(cfg: MemoryConfig): MemoryCategory[] {
44
- const out: MemoryCategory[] = [...USER_MEMORY_CATEGORIES];
45
- if (cfg.enableFullContextMemory) {
46
- out.push(
47
- FULL_CONTEXT_USER,
48
- FULL_CONTEXT_ASSISTANT,
49
- FULL_CONTEXT_SYSTEM,
50
- FULL_CONTEXT_TOOL,
51
- FULL_CONTEXT_TOOL_RESULT,
52
- FULL_CONTEXT_OTHERS,
53
- );
54
- }
55
- if (cfg.enableSelfImprovingMemory) {
56
- out.push(...SELF_IMPROVING_CATEGORIES);
57
- }
58
- return out;
59
- }
60
-
61
- function buildPanelConfigPayload(cfg: MemoryConfig) {
62
- return {
63
- enableFullContextMemory: cfg.enableFullContextMemory,
64
- enableSelfImprovingMemory: cfg.enableSelfImprovingMemory,
65
- categoryLabelsZh: { ...MEMORY_CATEGORY_LABEL_ZH },
66
- tabCategories: {
67
- user: [...USER_MEMORY_CATEGORIES],
68
- self: cfg.enableSelfImprovingMemory ? [...SELF_IMPROVING_CATEGORIES] : [],
69
- full: cfg.enableFullContextMemory ? [...FULL_CONTEXT_SOURCE_CATEGORIES] : [],
70
- },
71
- /** 各 Tab 列表「记忆类型」筛选项(来自插件配置 + 默认中文名) */
72
- memoryTypeFilterOptions: cfg.adminPanelMemoryTypeOptions,
73
- };
74
- }
14
+ import {
15
+ opMemoryAdminAdd,
16
+ opMemoryAdminConfig,
17
+ opMemoryAdminDashboard,
18
+ opMemoryAdminDelete,
19
+ opMemoryAdminFacets,
20
+ opMemoryAdminList,
21
+ type MemoryAdminOpsContext,
22
+ type MemoryAdminOpsOpts,
23
+ } from "./memory-admin-ops.js";
24
+
25
+ export type MemoryPanelRoutesOpts = MemoryAdminOpsOpts;
75
26
 
76
27
  export type RegisterHttpRoute = (params: {
77
28
  path: string;
@@ -83,6 +34,8 @@ export type RegisterHttpRoute = (params: {
83
34
 
84
35
  type PluginLogger = { info: (m: string) => void; warn: (m: string) => void };
85
36
 
37
+ type RegisterGatewayMethod = OpenClawPluginApi["registerGatewayMethod"];
38
+
86
39
  function resolveGatewayToken(): string | undefined {
87
40
  const stateDir = process.env.OPENCLAW_STATE_DIR ?? path.join(os.homedir(), ".openclaw");
88
41
  const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? path.join(stateDir, "openclaw.json");
@@ -109,16 +62,6 @@ function resolveGatewayToken(): string | undefined {
109
62
  return undefined;
110
63
  }
111
64
 
112
- function tabToCategories(tab: string, cfg: MemoryConfig): MemoryCategory[] {
113
- if (tab === "full") {
114
- return cfg.enableFullContextMemory ? [...FULL_CONTEXT_SOURCE_CATEGORIES] : [];
115
- }
116
- if (tab === "self") {
117
- return cfg.enableSelfImprovingMemory ? [...SELF_IMPROVING_CATEGORIES] : [];
118
- }
119
- return [...USER_MEMORY_CATEGORIES];
120
- }
121
-
122
65
  function parseUrl(req: IncomingMessage): URL {
123
66
  return new URL(req.url || "/", "http://" + (req.headers.host || "localhost"));
124
67
  }
@@ -140,24 +83,94 @@ function sendHtml(res: ServerResponse, html: string): void {
140
83
  res.end(html);
141
84
  }
142
85
 
143
- function readBody(req: IncomingMessage, maxBytes = 1024 * 64): Promise<string> {
144
- return new Promise((resolve, reject) => {
145
- let data = "";
146
- let bytes = 0;
147
- req.on("data", (chunk: Buffer) => {
148
- bytes += chunk.length;
149
- if (bytes > maxBytes) {
150
- req.destroy();
151
- reject(new Error("body too large"));
152
- return;
153
- }
154
- data += chunk.toString();
155
- });
156
- req.on("end", () => resolve(data));
157
- req.on("error", reject);
86
+ function applyAdminOpResult(respond: GatewayRequestHandlerOptions["respond"], result: import("./memory-admin-ops.js").AdminOpResult): void {
87
+ if (result.ok) {
88
+ respond(true, result.data);
89
+ return;
90
+ }
91
+ const msg =
92
+ typeof result.body.error === "string"
93
+ ? result.body.error
94
+ : JSON.stringify(result.body.error ?? result.body);
95
+ respond(false, { ...result.body, status: result.status }, { message: msg, code: `http_${result.status}` });
96
+ }
97
+
98
+ /** Gateway WebSocket methods: memory.admin.config | facets | dashboard | list | delete | add */
99
+ export function registerMemoryAdminGatewayMethods(
100
+ registerGatewayMethod: RegisterGatewayMethod,
101
+ db: MemoryDB,
102
+ cfg: MemoryConfig,
103
+ logger: PluginLogger,
104
+ opts?: MemoryPanelRoutesOpts | null,
105
+ ): void {
106
+ const ctxBase: MemoryAdminOpsContext = {
107
+ db,
108
+ cfg,
109
+ opts: opts ?? undefined,
110
+ };
111
+
112
+ registerGatewayMethod("memory.admin.config", async ({ params, respond }) => {
113
+ void params;
114
+ try {
115
+ applyAdminOpResult(respond, await opMemoryAdminConfig(ctxBase));
116
+ } catch (e) {
117
+ logger.warn(`memory.admin.config: ${String(e)}`);
118
+ respond(false, { error: String(e) });
119
+ }
120
+ });
121
+
122
+ registerGatewayMethod("memory.admin.facets", async ({ params, respond }) => {
123
+ void params;
124
+ try {
125
+ applyAdminOpResult(respond, await opMemoryAdminFacets(ctxBase));
126
+ } catch (e) {
127
+ logger.warn(`memory.admin.facets: ${String(e)}`);
128
+ respond(false, { error: String(e) });
129
+ }
130
+ });
131
+
132
+ registerGatewayMethod("memory.admin.dashboard", async ({ params, respond }) => {
133
+ try {
134
+ applyAdminOpResult(respond, await opMemoryAdminDashboard(ctxBase, params ?? {}));
135
+ } catch (e) {
136
+ logger.warn(`memory.admin.dashboard: ${String(e)}`);
137
+ respond(false, { error: String(e) });
138
+ }
139
+ });
140
+
141
+ registerGatewayMethod("memory.admin.list", async ({ params, respond }) => {
142
+ try {
143
+ applyAdminOpResult(respond, await opMemoryAdminList(ctxBase, params ?? {}));
144
+ } catch (e) {
145
+ logger.warn(`memory.admin.list: ${String(e)}`);
146
+ respond(false, { error: String(e) });
147
+ }
158
148
  });
149
+
150
+ registerGatewayMethod("memory.admin.delete", async ({ params, respond }) => {
151
+ try {
152
+ applyAdminOpResult(respond, await opMemoryAdminDelete(ctxBase, params ?? {}));
153
+ } catch (e) {
154
+ logger.warn(`memory.admin.delete: ${String(e)}`);
155
+ respond(false, { error: String(e) });
156
+ }
157
+ });
158
+
159
+ registerGatewayMethod("memory.admin.add", async ({ params, respond }) => {
160
+ try {
161
+ applyAdminOpResult(respond, await opMemoryAdminAdd(ctxBase, params ?? {}));
162
+ } catch (e) {
163
+ logger.warn(`memory.admin.add: ${String(e)}`);
164
+ respond(false, { error: String(e) });
165
+ }
166
+ });
167
+
168
+ logger.info("[openclaw-memory-alibaba-local] Memory admin Gateway methods: memory.admin.* (config, facets, dashboard, list, delete, add)");
159
169
  }
160
170
 
171
+ /**
172
+ * HTTP: serves HTML at /plugins/memory; legacy /plugins/memory/api/* returns 410 with migration hint.
173
+ */
161
174
  export function registerMemoryPanelRoutes(
162
175
  registerHttpRoute: RegisterHttpRoute,
163
176
  db: MemoryDB,
@@ -165,6 +178,9 @@ export function registerMemoryPanelRoutes(
165
178
  logger: PluginLogger,
166
179
  opts?: MemoryPanelRoutesOpts | null,
167
180
  ): void {
181
+ void db;
182
+ void cfg;
183
+ void opts;
168
184
  const requiredToken = resolveGatewayToken();
169
185
  const token = typeof requiredToken === "string" && requiredToken.length > 0 ? requiredToken : undefined;
170
186
 
@@ -178,237 +194,24 @@ export function registerMemoryPanelRoutes(
178
194
  const p = url.pathname;
179
195
  const isMemoryApi = p.startsWith("/plugins/memory/api/");
180
196
 
181
- // HTML shell is public so the browser can load the panel; JSON APIs stay token-protected.
182
- if (token && isMemoryApi) {
183
- const queryToken = (url.searchParams.get("token") ?? "").trim();
184
- const authHeader = (req.headers.authorization ?? req.headers.Authorization ?? "") as string;
185
- const bearer = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
186
- if (queryToken !== token && bearer !== token) {
187
- sendJson(res, 401, { error: { message: "Unauthorized", type: "unauthorized" } });
188
- return true;
189
- }
190
- }
191
-
192
- if (!isMemoryApi) {
193
- sendHtml(res, getMemoryPanelHtml());
194
- return true;
195
- }
196
-
197
- const tryDb = async (): Promise<boolean> => {
198
- try {
199
- await db.ensureReady();
200
- return true;
201
- } catch (e) {
202
- sendJson(res, 503, { error: "Database unavailable", detail: String(e) });
203
- return false;
204
- }
205
- };
206
- if (!(await tryDb())) {
207
- return true;
208
- }
209
-
210
- if (p === "/plugins/memory/api/config" && req.method === "GET") {
211
- sendJson(res, 200, buildPanelConfigPayload(cfg));
212
- return true;
213
- }
214
-
215
- if (p === "/plugins/memory/api/facets" && req.method === "GET") {
216
- try {
217
- // No category filter: include legacy `full_context_memory`, manual categories, and any future values.
218
- // List API still filters by tab category; dropdowns only need distinct agentId/sessionId from real rows.
219
- const facets = await db.listAdminFacets([], undefined, undefined);
220
- sendJson(res, 200, facets);
221
- } catch (e) {
222
- sendJson(res, 400, { error: String(e) });
223
- }
224
- return true;
225
- }
226
-
227
- if (p === "/plugins/memory/api/dashboard" && req.method === "GET") {
228
- const timeFromRaw = url.searchParams.get("timeFrom");
229
- const timeToRaw = url.searchParams.get("timeTo");
230
- const timeFromMs = parseOptionalTimeMs(timeFromRaw);
231
- const timeToMs = parseOptionalTimeMs(timeToRaw);
232
- if (timeFromMs === undefined || timeToMs === undefined) {
233
- sendJson(res, 400, { error: "timeFrom and timeTo are required (ISO 8601)" });
234
- return true;
235
- }
236
- const agentId = (url.searchParams.get("agentId") ?? "").trim();
237
- const sessionId = (url.searchParams.get("sessionId") ?? "").trim();
238
- if (!agentId) {
239
- sendJson(res, 400, { error: "缺少 agentId:请先选择 Agent" });
240
- return true;
241
- }
242
- try {
243
- const agg = await db.getAdminDashboardAggregates(timeFromMs, timeToMs, agentId, sessionId);
244
- sendJson(res, 200, agg);
245
- } catch (e) {
246
- sendJson(res, 400, { error: String(e) });
247
- }
248
- return true;
249
- }
250
-
251
- if (p === "/plugins/memory/api/list" && req.method === "GET") {
252
- const tab = url.searchParams.get("tab") || "user";
253
- const agentId = (url.searchParams.get("agentId") ?? "").trim();
254
- const sessionId = (url.searchParams.get("sessionId") ?? "").trim();
255
- if (!agentId) {
256
- sendJson(res, 400, { error: "缺少 agentId:请先选择 Agent" });
257
- return true;
258
- }
259
- const baseCats = tabToCategories(tab, cfg);
260
- if (baseCats.length === 0) {
261
- sendJson(res, 200, { items: [], total: 0, page: 1, pageSize: 100 });
262
- return true;
263
- }
264
- const timeFromMs = parseOptionalTimeMs(url.searchParams.get("timeFrom"));
265
- const timeToMs = parseOptionalTimeMs(url.searchParams.get("timeTo"));
266
- const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10) || 1);
267
- const pageSize = Math.min(500, Math.max(1, parseInt(url.searchParams.get("limit") || "100", 10) || 100));
268
-
269
- const categoryOne = (url.searchParams.get("category") ?? "").trim();
270
- let filterCats = baseCats;
271
- if (categoryOne) {
272
- if (!baseCats.includes(categoryOne as MemoryCategory)) {
273
- sendJson(res, 400, { error: "category 与当前 Tab 不匹配" });
274
- return true;
275
- }
276
- const listTab = tab === "full" ? "full" : tab === "self" ? "self" : "user";
277
- const optList = cfg.adminPanelMemoryTypeOptions[listTab];
278
- if (!optList.some((o) => o.category === categoryOne)) {
279
- sendJson(res, 400, { error: "category 不在插件配置的记忆类型筛选项中" });
280
- return true;
281
- }
282
- filterCats = [categoryOne as MemoryCategory];
283
- }
284
-
285
- const filters: AdminListFilters = {
286
- categories: filterCats,
287
- timeFromMs,
288
- timeToMs,
289
- };
290
- filters.agentId = agentId;
291
- if (sessionId) {
292
- filters.sessionId = sessionId;
293
- }
294
-
295
- try {
296
- const sortDesc = url.searchParams.get("sortDesc") !== "false";
297
- const adminTab = tab === "full" ? "full" : tab === "self" ? "self" : "user";
298
- const { total, items } = await db.listAdminFiltered(filters, page, pageSize, {
299
- adminTab,
300
- sortDesc,
301
- });
302
- sendJson(res, 200, {
303
- items,
304
- total,
305
- page,
306
- pageSize,
307
- });
308
- } catch (e) {
309
- sendJson(res, 400, { error: String(e) });
310
- }
311
- return true;
312
- }
313
-
314
- if (p === "/plugins/memory/api/delete" && req.method === "POST") {
315
- const raw = await readBody(req);
316
- let body: { items?: Array<{ agentId?: string; id?: string }> };
317
- try {
318
- body = JSON.parse(raw || "{}") as typeof body;
319
- } catch {
320
- sendJson(res, 400, { error: "Invalid JSON" });
321
- return true;
322
- }
323
- const items = Array.isArray(body.items) ? body.items : [];
324
- const normalized: { agentId: string; id: string }[] = [];
325
- for (const it of items) {
326
- if (it?.agentId && it?.id) {
327
- normalized.push({ agentId: String(it.agentId), id: String(it.id) });
328
- }
329
- }
330
- if (normalized.length === 0) {
331
- sendJson(res, 400, { error: "items required" });
332
- return true;
333
- }
334
- const n = await db.deleteMany(normalized);
335
- sendJson(res, 200, { deleted: n });
336
- return true;
337
- }
338
-
339
- // Manual insert: embed for user/self rows; full_context_* uses zero-vector placeholder (no embed).
340
- if (p === "/plugins/memory/api/add" && req.method === "POST") {
341
- const enc = opts?.encodeForStorage;
342
- const vectorDim = typeof opts?.vectorDim === "number" && opts.vectorDim > 0 ? opts.vectorDim : 768;
343
- const raw = await readBody(req);
344
- let body: { agentId?: string; text?: string; category?: string };
345
- try {
346
- body = JSON.parse(raw || "{}") as typeof body;
347
- } catch {
348
- sendJson(res, 400, { error: "Invalid JSON" });
349
- return true;
350
- }
351
- const agentId = (body.agentId ?? "").trim();
352
- const textRaw = body.text == null ? "" : String(body.text);
353
- const category = (body.category ?? "").trim() as MemoryCategory;
354
- if (!agentId) {
355
- sendJson(res, 400, { error: "agentId required" });
356
- return true;
357
- }
358
- const text = textRaw.trim();
359
- if (!text.length) {
360
- sendJson(res, 400, { error: "text required" });
361
- return true;
362
- }
363
- const allowed = new Set(writableCategoriesForPanel(cfg));
364
- if (!allowed.has(category)) {
365
- sendJson(res, 400, { error: "invalid or disabled category" });
366
- return true;
367
- }
368
- const textForEmbed = text.length > MANUAL_ADD_MAX_CHARS ? text.slice(0, MANUAL_ADD_MAX_CHARS) : text;
369
- const needsRealEmbed = categoryUsesRealEmbedding(category);
370
- if (needsRealEmbed && !enc) {
371
- sendJson(res, 503, { error: "Embedding not configured; plugin needs embedding in config." });
372
- return true;
373
- }
374
- let vectors: number[][];
375
- if (needsRealEmbed) {
376
- try {
377
- const out = await enc!(textForEmbed);
378
- vectors = out.vectors;
379
- } catch (e) {
380
- sendJson(res, 502, { error: `embed failed: ${String(e)}` });
197
+ if (isMemoryApi) {
198
+ if (token) {
199
+ const queryToken = (url.searchParams.get("token") ?? "").trim();
200
+ const authHeader = (req.headers.authorization ?? req.headers.Authorization ?? "") as string;
201
+ const bearer = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
202
+ if (queryToken !== token && bearer !== token) {
203
+ sendJson(res, 401, { error: { message: "Unauthorized", type: "unauthorized" } });
381
204
  return true;
382
205
  }
383
- if (vectors.length === 0) {
384
- sendJson(res, 400, { error: "nothing to embed (empty after chunking)" });
385
- return true;
386
- }
387
- } else {
388
- vectors = [Array.from({ length: vectorDim }, () => 0)];
389
206
  }
390
- const stored = await db.storeMany(
391
- agentId,
392
- vectors.map((vector, idx) => ({
393
- text: textForEmbed,
394
- vector,
395
- importance: 1,
396
- category,
397
- userId: "",
398
- sessionId: MANUAL_INSERT_SESSION,
399
- seqInBatch: 0,
400
- chunkIndex: idx,
401
- })),
402
- );
403
- sendJson(res, 200, {
404
- id: stored[0]!.id,
405
- createdAt: stored[0]!.createdAt,
406
- chunkRows: stored.length,
207
+ sendJson(res, 410, {
208
+ error: "HTTP JSON API removed",
209
+ detail: "Use Gateway WebSocket RPC methods: memory.admin.config, memory.admin.facets, memory.admin.dashboard, memory.admin.list, memory.admin.delete, memory.admin.add",
407
210
  });
408
211
  return true;
409
212
  }
410
213
 
411
- sendJson(res, 404, { error: "Not found" });
214
+ sendHtml(res, getMemoryPanelHtml());
412
215
  return true;
413
216
  } catch (err) {
414
217
  logger.warn(`openclaw-memory-alibaba-local memory panel: ${String(err)}`);
@@ -418,16 +221,5 @@ export function registerMemoryPanelRoutes(
418
221
  },
419
222
  });
420
223
 
421
- logger.info("[openclaw-memory-alibaba-local] Memory admin UI at /plugins/memory/");
422
- }
423
-
424
- function parseOptionalTimeMs(iso: string | null): number | undefined {
425
- if (!iso) {
426
- return undefined;
427
- }
428
- const t = Date.parse(iso);
429
- if (Number.isNaN(t)) {
430
- return undefined;
431
- }
432
- return t;
224
+ logger.info("[openclaw-memory-alibaba-local] Memory admin UI at /plugins/memory/ (data via WebSocket memory.admin.*)");
433
225
  }
package/web/memory-ui.ts CHANGED
@@ -798,8 +798,6 @@ function buildClientScript(): string {
798
798
  const s = `
799
799
  (function () {
800
800
  var LS_TOKEN_KEY = "openclaw_memory_gateway_token";
801
- /** Plugin routes are fixed; do not derive from pathname ("/" 首页或 hash 路由会错成 /api)。 */
802
- var API_BASE = "/plugins/memory/api";
803
801
 
804
802
  function readTokenFromQueryString(qs) {
805
803
  if (!qs || qs.charAt(0) !== "?") {
@@ -836,42 +834,234 @@ function buildClientScript(): string {
836
834
  }
837
835
  }
838
836
 
839
- function withAuth(url) {
840
- var u = url.indexOf("?") >= 0 ? url + "&" : url + "?";
841
- var tok = getGatewayToken();
842
- if (tok) {
843
- u += "token=" + encodeURIComponent(tok) + "&";
837
+ function gatewayWsUrl() {
838
+ return (location.protocol === "https:" ? "wss:" : "ws:") + "//" + location.host;
839
+ }
840
+
841
+ /** 与网关 isLocalClient 一致:仅这些主机用 Control UI 客户端 + allowInsecureAuth 即可;其它主机需 probe + dangerouslyDisableDeviceAuth 等 */
842
+ function isMemoryPanelLoopbackHost() {
843
+ var h = (location.hostname || "").toLowerCase();
844
+ return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]";
845
+ }
846
+
847
+ function gatewayPanelConnectClient() {
848
+ if (isMemoryPanelLoopbackHost()) {
849
+ return { id: "openclaw-control-ui", mode: "ui" };
844
850
  }
845
- return u.replace(/[&?]$/, "").replace("?&", "?");
851
+ return { id: "openclaw-probe", mode: "probe" };
846
852
  }
847
853
 
848
- /** 避免页面带 <base href> 时相对路径跑偏;网关层也认 Bearer。 */
849
- function memoryApiAbsolute(pathWithLeadingSlash) {
850
- var p = pathWithLeadingSlash.charAt(0) === "/" ? pathWithLeadingSlash : "/" + pathWithLeadingSlash;
851
- return window.location.origin + API_BASE + p;
854
+ function hintGatewayControlUiInsecureAuth() {
855
+ if (isMemoryPanelLoopbackHost()) {
856
+ return (
857
+ " 解决办法:编辑 ~/.openclaw/openclaw.json,在 gateway.controlUi 中加入 \\"allowInsecureAuth\\": true,保存后执行 openclaw gateway restart,再刷新本页。" +
858
+ " 请用 http://127.0.0.1 或 http://localhost 打开面板(不要用局域网 IP,除非按下文配置)。"
859
+ );
860
+ }
861
+ return (
862
+ " 解决办法(任选其一):(1) 用本机浏览器打开 http://127.0.0.1:<网关端口>/plugins/memory?token=…,并设置 gateway.controlUi.allowInsecureAuth 为 true;" +
863
+ "(2) 若必须从其它机器/局域网 IP 访问,在 gateway.controlUi 设置 \\"dangerouslyDisableDeviceAuth\\": true 后重启 gateway(会降低设备绑定安全策略);" +
864
+ "(3) 使用 HTTPS 并完成网关设备配对。"
865
+ );
852
866
  }
853
867
 
854
- function authFetchHeaders(base) {
855
- var h = {};
856
- if (base) {
857
- for (var k in base) {
858
- if (Object.prototype.hasOwnProperty.call(base, k)) {
859
- h[k] = base[k];
860
- }
868
+ var gwState = {
869
+ ws: null,
870
+ pending: new Map(),
871
+ phase: "off",
872
+ connectPromise: null,
873
+ reqSeq: 0,
874
+ connectResolve: null,
875
+ connectReject: null
876
+ };
877
+
878
+ function gwNextId() {
879
+ gwState.reqSeq += 1;
880
+ return "m" + gwState.reqSeq + "-" + Date.now();
881
+ }
882
+
883
+ function gwFailAllPending(err) {
884
+ gwState.pending.forEach(function (fn) {
885
+ try {
886
+ fn({ ok: false, error: { message: String(err && err.message ? err.message : err) } });
887
+ } catch (e) {}
888
+ });
889
+ gwState.pending.clear();
890
+ }
891
+
892
+ function gwOnMessage(ev) {
893
+ var msg;
894
+ try {
895
+ msg = JSON.parse(ev.data);
896
+ } catch (e) {
897
+ return;
898
+ }
899
+ if (gwState.phase === "wait-challenge") {
900
+ if (msg.type === "event" && msg.event === "connect.challenge") {
901
+ gwState.phase = "wait-connect-res";
902
+ var cid = gwNextId();
903
+ var tok = getGatewayToken();
904
+ gwState.pending.set(cid, function (resMsg) {
905
+ if (!resMsg.ok) {
906
+ gwState.phase = "failed";
907
+ var em = (resMsg.error && resMsg.error.message) ? String(resMsg.error.message) : "connect failed";
908
+ if (gwState.connectReject) gwState.connectReject(new Error(em));
909
+ return;
910
+ }
911
+ gwState.phase = "ready";
912
+ if (gwState.connectResolve) gwState.connectResolve();
913
+ });
914
+ var gwc = gatewayPanelConnectClient();
915
+ gwState.ws.send(
916
+ JSON.stringify({
917
+ type: "req",
918
+ id: cid,
919
+ method: "connect",
920
+ params: {
921
+ minProtocol: 3,
922
+ maxProtocol: 3,
923
+ client: {
924
+ id: gwc.id,
925
+ version: "memory-panel",
926
+ platform: typeof navigator !== "undefined" && navigator.platform ? navigator.platform : "web",
927
+ mode: gwc.mode
928
+ },
929
+ role: "operator",
930
+ scopes: ["operator.admin"],
931
+ auth: tok ? { token: tok } : {}
932
+ }
933
+ })
934
+ );
861
935
  }
936
+ return;
862
937
  }
863
- var tok = getGatewayToken();
864
- if (tok) {
865
- h["Authorization"] = "Bearer " + tok;
938
+ if (msg.type === "res" && gwState.pending.has(msg.id)) {
939
+ var fn = gwState.pending.get(msg.id);
940
+ gwState.pending.delete(msg.id);
941
+ fn(msg);
866
942
  }
867
- return h;
868
943
  }
869
944
 
870
- function fetchMemoryApi(pathWithLeadingSlash, init) {
871
- var url = withAuth(memoryApiAbsolute(pathWithLeadingSlash));
872
- var o = init ? Object.assign({}, init) : {};
873
- o.headers = authFetchHeaders(o.headers || {});
874
- return fetch(url, o);
945
+ function ensureGatewayConnected() {
946
+ if (gwState.phase === "ready" && gwState.ws && gwState.ws.readyState === 1) {
947
+ return Promise.resolve();
948
+ }
949
+ if (gwState.connectPromise) {
950
+ return gwState.connectPromise;
951
+ }
952
+ if (gwState.ws) {
953
+ try {
954
+ gwState.ws.close();
955
+ } catch (e) {}
956
+ gwState.ws = null;
957
+ }
958
+ gwState.phase = "wait-challenge";
959
+ gwState.pending.clear();
960
+ gwState.connectPromise = new Promise(function (resolve, reject) {
961
+ gwState.connectResolve = resolve;
962
+ gwState.connectReject = reject;
963
+ try {
964
+ gwState.ws = new WebSocket(gatewayWsUrl());
965
+ } catch (e) {
966
+ gwState.phase = "failed";
967
+ gwState.connectPromise = null;
968
+ reject(e);
969
+ return;
970
+ }
971
+ gwState.ws.onmessage = gwOnMessage;
972
+ gwState.ws.onerror = function () {
973
+ if (gwState.phase !== "ready") {
974
+ gwState.phase = "failed";
975
+ gwState.connectPromise = null;
976
+ if (gwState.connectReject) gwState.connectReject(new Error("WebSocket error"));
977
+ }
978
+ };
979
+ gwState.ws.onclose = function () {
980
+ var wasReady = gwState.phase === "ready";
981
+ gwState.phase = "off";
982
+ gwState.ws = null;
983
+ gwState.connectPromise = null;
984
+ if (wasReady) {
985
+ gwFailAllPending(new Error("WebSocket closed"));
986
+ }
987
+ };
988
+ });
989
+ return gwState.connectPromise.finally(function () {
990
+ gwState.connectPromise = null;
991
+ gwState.connectResolve = null;
992
+ gwState.connectReject = null;
993
+ });
994
+ }
995
+
996
+ function gatewayRpc(method, params) {
997
+ return ensureGatewayConnected().then(function () {
998
+ return new Promise(function (resolve, reject) {
999
+ if (!gwState.ws || gwState.ws.readyState !== 1) {
1000
+ reject(new Error("WebSocket not open"));
1001
+ return;
1002
+ }
1003
+ var id = gwNextId();
1004
+ gwState.pending.set(id, function (msg) {
1005
+ if (msg.ok) {
1006
+ resolve({ ok: true, data: msg.payload });
1007
+ } else {
1008
+ var em = msg.error && msg.error.message ? String(msg.error.message) : "rpc error";
1009
+ var low = em.toLowerCase();
1010
+ var unauth =
1011
+ low.indexOf("unauthorized") >= 0 ||
1012
+ low.indexOf("token mismatch") >= 0 ||
1013
+ low.indexOf("token missing") >= 0 ||
1014
+ low.indexOf("missing scope") >= 0;
1015
+ resolve({ ok: false, unauthorized: unauth, message: em, payload: msg.payload });
1016
+ }
1017
+ });
1018
+ gwState.ws.send(JSON.stringify({ type: "req", id: id, method: method, params: params || {} }));
1019
+ });
1020
+ });
1021
+ }
1022
+
1023
+ /** 与原先 fetch 类似的接口:ok / status / json() / text() */
1024
+ async function fetchMemoryApi(method, params) {
1025
+ try {
1026
+ var res = await gatewayRpc(method, params || {});
1027
+ if (res.ok) {
1028
+ return {
1029
+ ok: true,
1030
+ status: 200,
1031
+ json: function () {
1032
+ return Promise.resolve(res.data);
1033
+ },
1034
+ text: function () {
1035
+ return Promise.resolve("");
1036
+ }
1037
+ };
1038
+ }
1039
+ var st = res.payload && res.payload.status ? parseInt(String(res.payload.status), 10) : 0;
1040
+ if (!Number.isFinite(st) || st < 400) {
1041
+ st = res.unauthorized ? 403 : 502;
1042
+ }
1043
+ return {
1044
+ ok: false,
1045
+ status: st,
1046
+ json: function () {
1047
+ return Promise.resolve(res.payload && (res.payload.error || res.payload) ? res.payload : { error: res.message });
1048
+ },
1049
+ text: function () {
1050
+ return Promise.resolve(res.message || "");
1051
+ }
1052
+ };
1053
+ } catch (e) {
1054
+ return {
1055
+ ok: false,
1056
+ status: 0,
1057
+ json: function () {
1058
+ return Promise.resolve({ error: String(e) });
1059
+ },
1060
+ text: function () {
1061
+ return Promise.resolve(String(e));
1062
+ }
1063
+ };
1064
+ }
875
1065
  }
876
1066
 
877
1067
  function showBanner(kind, msg) {
@@ -997,35 +1187,28 @@ function buildClientScript(): string {
997
1187
  };
998
1188
  }
999
1189
 
1000
- function facetQueryPath() {
1001
- var q = new URLSearchParams();
1002
- q.set("tab", state.tab);
1003
- return "/facets?" + q.toString();
1004
- }
1005
-
1006
- function listQueryPath(page, tr) {
1190
+ function listQueryParams(page, tr) {
1007
1191
  var t = tr != null ? tr : readTimeRange();
1008
1192
  if (t.error) {
1009
1193
  return t;
1010
1194
  }
1011
- var q = new URLSearchParams();
1012
- q.set("tab", state.tab);
1013
1195
  var aid = state.selectedAgentId.trim();
1014
1196
  var sid = state.selectedSessionId.trim();
1015
- q.set("agentId", aid);
1016
- if (sid) q.set("sessionId", sid);
1017
- q.set("timeFrom", t.timeFrom);
1018
- q.set("timeTo", t.timeTo);
1019
- q.set("page", String(page || 1));
1020
- q.set("limit", String(state.pageSize));
1021
1197
  var ord = document.getElementById("sortOrder");
1022
- var sortDesc = ord && ord.value === "desc";
1023
- q.set("sortDesc", sortDesc ? "true" : "false");
1198
+ var sortDesc = !!(ord && ord.value === "desc");
1024
1199
  var catF = state.categoryFilterByTab[state.tab] || "";
1025
- if (catF) {
1026
- q.set("category", catF);
1027
- }
1028
- return { path: "/list?" + q.toString() };
1200
+ var o = {
1201
+ tab: state.tab,
1202
+ agentId: aid,
1203
+ timeFrom: t.timeFrom,
1204
+ timeTo: t.timeTo,
1205
+ page: page || 1,
1206
+ limit: state.pageSize,
1207
+ sortDesc: sortDesc
1208
+ };
1209
+ if (sid) o.sessionId = sid;
1210
+ if (catF) o.category = catF;
1211
+ return { params: o };
1029
1212
  }
1030
1213
 
1031
1214
  function syncMemoryTypeFilterSelect() {
@@ -1096,10 +1279,38 @@ function buildClientScript(): string {
1096
1279
  }
1097
1280
 
1098
1281
  async function loadConfig() {
1099
- var r = await fetchMemoryApi("/config");
1282
+ var r = await fetchMemoryApi("memory.admin.config", {});
1100
1283
  if (!r.ok) {
1101
- if (r.status === 401) {
1102
- throw new Error("未授权:请在 URL 使用 ?token=(与 ~/.openclaw/openclaw.json 中 gateway.auth.token 一致),hash 路由可用 #/path?token=;成功一次后会写入本机 localStorage。");
1284
+ if (r.status === 401 || r.status === 403) {
1285
+ var detailAuth = await r.json().catch(function () {
1286
+ return {};
1287
+ });
1288
+ var errLow = String(detailAuth.error || "").toLowerCase();
1289
+ var hintScope =
1290
+ r.status === 403 && errLow.indexOf("scope") >= 0
1291
+ ? isMemoryPanelLoopbackHost()
1292
+ ? " 已通过 token 连接但仍缺少 scope:请在 openclaw.json 的 gateway.controlUi 设置 allowInsecureAuth: true(回环访问),保存后重启 gateway。"
1293
+ : " 已通过 token 连接但仍缺少 scope:非本机 hostname 访问时请设置 gateway.controlUi.dangerouslyDisableDeviceAuth: true,或改用 http://127.0.0.1 打开面板并配合 allowInsecureAuth: true;保存后重启 gateway。"
1294
+ : "";
1295
+ throw new Error(
1296
+ (r.status === 401
1297
+ ? "未授权:请在 URL 使用 ?token=(与 ~/.openclaw/openclaw.json 中 gateway.auth.token 一致),hash 路由可用 #/path?token=;面板通过 WebSocket 连接网关时会在 connect 中携带该 token。成功一次后会写入本机 localStorage。"
1298
+ : "拒绝访问:" + (detailAuth.error ? String(detailAuth.error) : (await r.text().catch(function () { return ""; }))) + hintScope)
1299
+ );
1300
+ }
1301
+ if (r.status === 0) {
1302
+ var detail0 = await r.json().catch(function () {
1303
+ return {};
1304
+ });
1305
+ var msg0 = detail0 && detail0.error != null ? String(detail0.error) : await r.text().catch(function () { return ""; });
1306
+ var low0 = msg0.toLowerCase();
1307
+ var hintHandshake =
1308
+ low0.indexOf("device identity") >= 0 || low0.indexOf("control ui") >= 0 ? hintGatewayControlUiInsecureAuth() : "";
1309
+ throw new Error(
1310
+ msg0
1311
+ ? "无法连接网关 WebSocket 或握手失败:" + msg0 + hintHandshake
1312
+ : "无法连接网关 WebSocket(请确认网关已启动、页面与网关同源,且 token 正确)。仍失败请查看 gateway 日志。"
1313
+ );
1103
1314
  }
1104
1315
  throw new Error("config " + r.status);
1105
1316
  }
@@ -1446,16 +1657,15 @@ function buildClientScript(): string {
1446
1657
  }
1447
1658
  showBanner("", "");
1448
1659
  state.loading = true;
1449
- var q = new URLSearchParams();
1450
- q.set("timeFrom", tr.timeFrom);
1451
- q.set("timeTo", tr.timeTo);
1452
- q.set("agentId", aid);
1453
1660
  var sid = state.selectedSessionId.trim();
1454
- if (sid) {
1455
- q.set("sessionId", sid);
1456
- }
1457
1661
  try {
1458
- var r = await fetchMemoryApi("/dashboard?" + q.toString());
1662
+ var dashParams = {
1663
+ timeFrom: tr.timeFrom,
1664
+ timeTo: tr.timeTo,
1665
+ agentId: aid
1666
+ };
1667
+ if (sid) dashParams.sessionId = sid;
1668
+ var r = await fetchMemoryApi("memory.admin.dashboard", dashParams);
1459
1669
  var data = await r.json().catch(function () {
1460
1670
  return {};
1461
1671
  });
@@ -1474,11 +1684,17 @@ function buildClientScript(): string {
1474
1684
  async function loadFacets(opts) {
1475
1685
  var autoLoad = !opts || opts.autoLoad !== false;
1476
1686
  showBanner("", "");
1477
- var r = await fetchMemoryApi(facetQueryPath());
1687
+ var r = await fetchMemoryApi("memory.admin.facets", { tab: state.tab });
1478
1688
  if (!r.ok) {
1479
1689
  var err = await r.text();
1480
1690
  var hint401 = r.status === 401 ? " 需要 Token:URL ?token= 或 #/path?token=(与 gateway.auth.token 一致)" : "";
1481
- showBanner("err", "加载下拉失败: " + r.status + " " + err + hint401);
1691
+ var hint403 =
1692
+ r.status === 403 && String(err).toLowerCase().indexOf("scope") >= 0
1693
+ ? isMemoryPanelLoopbackHost()
1694
+ ? " 请在 openclaw.json 设置 gateway.controlUi.allowInsecureAuth: true 后重启 gateway。"
1695
+ : " 请在 openclaw.json 设置 gateway.controlUi.dangerouslyDisableDeviceAuth: true(或改用 127.0.0.1 打开)后重启 gateway。"
1696
+ : "";
1697
+ showBanner("err", "加载下拉失败: " + r.status + " " + err + hint401 + hint403);
1482
1698
  return;
1483
1699
  }
1484
1700
  var data = await r.json().catch(function () {
@@ -1547,8 +1763,13 @@ function buildClientScript(): string {
1547
1763
  state.loading = true;
1548
1764
  state.page = page || 1;
1549
1765
  try {
1550
- var lq = listQueryPath(state.page, tr);
1551
- var r = await fetchMemoryApi(lq.path);
1766
+ var lq = listQueryParams(state.page, tr);
1767
+ if (lq.error) {
1768
+ showBanner("err", lq.error);
1769
+ document.getElementById("pager").style.display = "none";
1770
+ return;
1771
+ }
1772
+ var r = await fetchMemoryApi("memory.admin.list", lq.params);
1552
1773
  var data = await r.json().catch(function () { return {}; });
1553
1774
  if (!r.ok) {
1554
1775
  showBanner("err", (data && data.error) ? String(data.error) : "列表请求失败 " + r.status);
@@ -1891,12 +2112,7 @@ function buildClientScript(): string {
1891
2112
  });
1892
2113
  if (!sel.length) return;
1893
2114
  if (!window.confirm("确认永久删除所选 " + sel.length + " 条?此操作不可恢复。")) return;
1894
- var body = JSON.stringify({ items: sel });
1895
- var r = await fetchMemoryApi("/delete", {
1896
- method: "POST",
1897
- headers: { "Content-Type": "application/json" },
1898
- body: body
1899
- });
2115
+ var r = await fetchMemoryApi("memory.admin.delete", { items: sel });
1900
2116
  var data = await r.json().catch(function () { return {}; });
1901
2117
  if (!r.ok) {
1902
2118
  showBanner("err", (data && data.error) ? String(data.error) : "删除失败 " + r.status);
@@ -1928,11 +2144,7 @@ function buildClientScript(): string {
1928
2144
  return;
1929
2145
  }
1930
2146
  showBanner("", "");
1931
- var r = await fetchMemoryApi("/add", {
1932
- method: "POST",
1933
- headers: { "Content-Type": "application/json" },
1934
- body: JSON.stringify({ agentId: agentId, category: category, text: text })
1935
- });
2147
+ var r = await fetchMemoryApi("memory.admin.add", { agentId: agentId, category: category, text: text });
1936
2148
  var data = await r.json().catch(function () { return {}; });
1937
2149
  if (!r.ok) {
1938
2150
  showBanner("err", (data && data.error) ? String(data.error) : "添加失败 " + r.status);