openclaw-memory-alibaba-local 0.1.3 → 0.1.5
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/API.md +9 -3
- package/package.json +1 -1
- package/web/memory-routes.ts +90 -9
- package/web/memory-ui.ts +164 -10
package/API.md
CHANGED
|
@@ -99,11 +99,17 @@
|
|
|
99
99
|
| `memory.admin.delete` | 批量删除 | `items`: `{ agentId, id }[]` | 同原 `POST …/api/delete` |
|
|
100
100
|
| `memory.admin.add` | 手工插入 | `agentId`, `text`, `category` | 同原 `POST …/api/add` |
|
|
101
101
|
|
|
102
|
-
### 1.3
|
|
102
|
+
### 1.3 HTTP 同源 RPC(公网 / 非本机,免 WS `operator.admin`)
|
|
103
103
|
|
|
104
104
|
| 路径 | 行为 |
|
|
105
105
|
|------|------|
|
|
106
|
-
| `
|
|
106
|
+
| `POST {GATEWAY}/plugins/memory/api/v1/call` | Body:`{"method":"memory.admin.list","params":{...}}`(`method` 与上表 WebSocket 方法名一致)。若配置了 `gateway.auth.token`,须 `Authorization: Bearer <token>` 或 URL `?token=`。成功时 HTTP 200,body 为与 WS `payload` 相同的 JSON;失败时 HTTP 状态码与 body 内 `error` 等与运维操作一致。 |
|
|
107
|
+
|
|
108
|
+
### 1.4 废弃的旧版 HTTP JSON 路径
|
|
109
|
+
|
|
110
|
+
| 路径 | 行为 |
|
|
111
|
+
|------|------|
|
|
112
|
+
| `GET/POST {GATEWAY}/plugins/memory/api/config` 等旧路径(非 `api/v1/call`) | 若配置了 gateway token,仍校验 `?token=` 或 `Authorization: Bearer`;未命中 `v1/call` 时返回 **`404`**,请使用上表 **`POST …/api/v1/call`** 或 WebSocket `memory.admin.*`。 |
|
|
107
113
|
|
|
108
114
|
---
|
|
109
115
|
|
|
@@ -113,7 +119,7 @@
|
|
|
113
119
|
|
|
114
120
|
| 项目 | 说明 |
|
|
115
121
|
|------|------|
|
|
116
|
-
| **功能** | 返回记忆管理端单页 HTML
|
|
122
|
+
| **功能** | 返回记忆管理端单页 HTML(壳页面;本机数据可走 WebSocket `memory.admin.*`,**非本机 hostname** 时面板走 **`POST /plugins/memory/api/v1/call`**) |
|
|
117
123
|
| **URI** | `{GATEWAY}/plugins/memory` |
|
|
118
124
|
|
|
119
125
|
**入参**
|
package/package.json
CHANGED
package/web/memory-routes.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
opMemoryAdminDelete,
|
|
19
19
|
opMemoryAdminFacets,
|
|
20
20
|
opMemoryAdminList,
|
|
21
|
+
type AdminOpResult,
|
|
21
22
|
type MemoryAdminOpsContext,
|
|
22
23
|
type MemoryAdminOpsOpts,
|
|
23
24
|
} from "./memory-admin-ops.js";
|
|
@@ -83,7 +84,7 @@ function sendHtml(res: ServerResponse, html: string): void {
|
|
|
83
84
|
res.end(html);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function applyAdminOpResult(respond: GatewayRequestHandlerOptions["respond"], result:
|
|
87
|
+
function applyAdminOpResult(respond: GatewayRequestHandlerOptions["respond"], result: AdminOpResult): void {
|
|
87
88
|
if (result.ok) {
|
|
88
89
|
respond(true, result.data);
|
|
89
90
|
return;
|
|
@@ -168,8 +169,49 @@ export function registerMemoryAdminGatewayMethods(
|
|
|
168
169
|
logger.info("[openclaw-memory-alibaba-local] Memory admin Gateway methods: memory.admin.* (config, facets, dashboard, list, delete, add)");
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
async function readJsonBody(req: IncomingMessage, maxBytes = 2 * 1024 * 1024): Promise<string> {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const chunks: Buffer[] = [];
|
|
175
|
+
let n = 0;
|
|
176
|
+
req.on("data", (chunk: Buffer) => {
|
|
177
|
+
n += chunk.length;
|
|
178
|
+
if (n > maxBytes) {
|
|
179
|
+
req.destroy();
|
|
180
|
+
reject(new Error("body too large"));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
chunks.push(chunk);
|
|
184
|
+
});
|
|
185
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
186
|
+
req.on("error", reject);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function dispatchMemoryAdminRpc(
|
|
191
|
+
ctx: MemoryAdminOpsContext,
|
|
192
|
+
method: string,
|
|
193
|
+
params: unknown,
|
|
194
|
+
): Promise<AdminOpResult> {
|
|
195
|
+
switch (method) {
|
|
196
|
+
case "memory.admin.config":
|
|
197
|
+
return opMemoryAdminConfig(ctx);
|
|
198
|
+
case "memory.admin.facets":
|
|
199
|
+
return opMemoryAdminFacets(ctx);
|
|
200
|
+
case "memory.admin.dashboard":
|
|
201
|
+
return opMemoryAdminDashboard(ctx, (params as Record<string, unknown>) ?? {});
|
|
202
|
+
case "memory.admin.list":
|
|
203
|
+
return opMemoryAdminList(ctx, (params as Record<string, unknown>) ?? {});
|
|
204
|
+
case "memory.admin.delete":
|
|
205
|
+
return opMemoryAdminDelete(ctx, (params as Record<string, unknown>) ?? {});
|
|
206
|
+
case "memory.admin.add":
|
|
207
|
+
return opMemoryAdminAdd(ctx, (params as Record<string, unknown>) ?? {});
|
|
208
|
+
default:
|
|
209
|
+
return { ok: false, status: 404, body: { error: "unknown method", method } };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
171
213
|
/**
|
|
172
|
-
* HTTP:
|
|
214
|
+
* HTTP: HTML at /plugins/memory;同源 HTTP POST /plugins/memory/api/v1/call 供非本机访问(仅需 gateway token,不经 WS operator scope)。
|
|
173
215
|
*/
|
|
174
216
|
export function registerMemoryPanelRoutes(
|
|
175
217
|
registerHttpRoute: RegisterHttpRoute,
|
|
@@ -178,9 +220,11 @@ export function registerMemoryPanelRoutes(
|
|
|
178
220
|
logger: PluginLogger,
|
|
179
221
|
opts?: MemoryPanelRoutesOpts | null,
|
|
180
222
|
): void {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
223
|
+
const ctxBase: MemoryAdminOpsContext = {
|
|
224
|
+
db,
|
|
225
|
+
cfg,
|
|
226
|
+
opts: opts ?? undefined,
|
|
227
|
+
};
|
|
184
228
|
const requiredToken = resolveGatewayToken();
|
|
185
229
|
const token = typeof requiredToken === "string" && requiredToken.length > 0 ? requiredToken : undefined;
|
|
186
230
|
|
|
@@ -204,9 +248,44 @@ export function registerMemoryPanelRoutes(
|
|
|
204
248
|
return true;
|
|
205
249
|
}
|
|
206
250
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
251
|
+
|
|
252
|
+
if (p === "/plugins/memory/api/v1/call" && req.method === "POST") {
|
|
253
|
+
let raw: string;
|
|
254
|
+
try {
|
|
255
|
+
raw = await readJsonBody(req);
|
|
256
|
+
} catch {
|
|
257
|
+
sendJson(res, 400, { error: "invalid body" });
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
let body: { method?: string; params?: unknown };
|
|
261
|
+
try {
|
|
262
|
+
body = JSON.parse(raw || "{}") as { method?: string; params?: unknown };
|
|
263
|
+
} catch {
|
|
264
|
+
sendJson(res, 400, { error: "invalid json" });
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
const method = typeof body.method === "string" ? body.method.trim() : "";
|
|
268
|
+
if (!method) {
|
|
269
|
+
sendJson(res, 400, { error: "missing method" });
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const result = await dispatchMemoryAdminRpc(ctxBase, method, body.params ?? {});
|
|
274
|
+
if (result.ok) {
|
|
275
|
+
sendJson(res, 200, result.data as unknown);
|
|
276
|
+
} else {
|
|
277
|
+
sendJson(res, result.status, { ...result.body, status: result.status });
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
logger.warn(`memory api/v1/call ${method}: ${String(e)}`);
|
|
281
|
+
sendJson(res, 500, { error: String(e) });
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
sendJson(res, 404, {
|
|
287
|
+
error: "not found",
|
|
288
|
+
detail: "POST /plugins/memory/api/v1/call with JSON { method, params } (same as memory.admin.* WebSocket RPC)",
|
|
210
289
|
});
|
|
211
290
|
return true;
|
|
212
291
|
}
|
|
@@ -221,5 +300,7 @@ export function registerMemoryPanelRoutes(
|
|
|
221
300
|
},
|
|
222
301
|
});
|
|
223
302
|
|
|
224
|
-
logger.info(
|
|
303
|
+
logger.info(
|
|
304
|
+
"[openclaw-memory-alibaba-local] Memory admin UI at /plugins/memory/ (WS memory.admin.* on loopback; HTTP POST .../api/v1/call for remote)",
|
|
305
|
+
);
|
|
225
306
|
}
|
package/web/memory-ui.ts
CHANGED
|
@@ -797,6 +797,20 @@ function buildClientScript(): string {
|
|
|
797
797
|
// 外层是模板字符串反引号:内层不要用 \ 转义双引号,否则生成到浏览器里的 JS 会断串(例如 class="mono")。
|
|
798
798
|
const s = `
|
|
799
799
|
(function () {
|
|
800
|
+
// 与 rds-claw-observability-plugin 一致:127.0.0.1 与 localhost 在网关 Control UI / WS 策略上不等价,统一跳到 localhost。
|
|
801
|
+
try {
|
|
802
|
+
if (window.location.hostname === "127.0.0.1") {
|
|
803
|
+
var _ocRedir =
|
|
804
|
+
window.location.protocol +
|
|
805
|
+
"//localhost" +
|
|
806
|
+
(window.location.port ? ":" + window.location.port : "") +
|
|
807
|
+
window.location.pathname +
|
|
808
|
+
window.location.search +
|
|
809
|
+
window.location.hash;
|
|
810
|
+
window.location.replace(_ocRedir);
|
|
811
|
+
}
|
|
812
|
+
} catch (_ocRedirErr) {}
|
|
813
|
+
|
|
800
814
|
var LS_TOKEN_KEY = "openclaw_memory_gateway_token";
|
|
801
815
|
|
|
802
816
|
function readTokenFromQueryString(qs) {
|
|
@@ -807,12 +821,60 @@ function buildClientScript(): string {
|
|
|
807
821
|
return t && String(t).trim() ? String(t).trim() : "";
|
|
808
822
|
}
|
|
809
823
|
|
|
824
|
+
/** 与观测插件一致:支持 JSON 包一层 token / gatewayToken / settings.* */
|
|
825
|
+
function tryTokenFromJsonString(raw) {
|
|
826
|
+
if (!raw || typeof raw !== "string") {
|
|
827
|
+
return "";
|
|
828
|
+
}
|
|
829
|
+
var st = raw.trim();
|
|
830
|
+
if (!st) {
|
|
831
|
+
return "";
|
|
832
|
+
}
|
|
833
|
+
if (st.charAt(0) === "{") {
|
|
834
|
+
try {
|
|
835
|
+
var obj = JSON.parse(st);
|
|
836
|
+
if (obj && typeof obj === "object") {
|
|
837
|
+
if (typeof obj.token === "string" && obj.token.trim()) {
|
|
838
|
+
return obj.token.trim();
|
|
839
|
+
}
|
|
840
|
+
if (typeof obj.gatewayToken === "string" && obj.gatewayToken.trim()) {
|
|
841
|
+
return obj.gatewayToken.trim();
|
|
842
|
+
}
|
|
843
|
+
if (obj.settings && typeof obj.settings === "object") {
|
|
844
|
+
if (typeof obj.settings.token === "string" && obj.settings.token.trim()) {
|
|
845
|
+
return obj.settings.token.trim();
|
|
846
|
+
}
|
|
847
|
+
if (typeof obj.settings.gatewayToken === "string" && obj.settings.gatewayToken.trim()) {
|
|
848
|
+
return obj.settings.gatewayToken.trim();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} catch (e) {}
|
|
853
|
+
}
|
|
854
|
+
return st;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function readTokenFromStorage(storage, key) {
|
|
858
|
+
try {
|
|
859
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
860
|
+
return "";
|
|
861
|
+
}
|
|
862
|
+
return tryTokenFromJsonString(storage.getItem(key) || "");
|
|
863
|
+
} catch (e) {
|
|
864
|
+
return "";
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function persistToken(t) {
|
|
869
|
+
try {
|
|
870
|
+
localStorage.setItem(LS_TOKEN_KEY, t);
|
|
871
|
+
} catch (e) {}
|
|
872
|
+
}
|
|
873
|
+
|
|
810
874
|
function getGatewayToken() {
|
|
811
875
|
var fromSearch = readTokenFromQueryString(window.location.search);
|
|
812
876
|
if (fromSearch) {
|
|
813
|
-
|
|
814
|
-
localStorage.setItem(LS_TOKEN_KEY, fromSearch);
|
|
815
|
-
} catch (e) {}
|
|
877
|
+
persistToken(fromSearch);
|
|
816
878
|
return fromSearch;
|
|
817
879
|
}
|
|
818
880
|
var hash = window.location.hash || "";
|
|
@@ -820,15 +882,35 @@ function buildClientScript(): string {
|
|
|
820
882
|
if (qm >= 0) {
|
|
821
883
|
var fromHash = readTokenFromQueryString(hash.slice(qm));
|
|
822
884
|
if (fromHash) {
|
|
823
|
-
|
|
824
|
-
localStorage.setItem(LS_TOKEN_KEY, fromHash);
|
|
825
|
-
} catch (e) {}
|
|
885
|
+
persistToken(fromHash);
|
|
826
886
|
return fromHash;
|
|
827
887
|
}
|
|
828
888
|
}
|
|
889
|
+
var ocKeys = [
|
|
890
|
+
"openclaw.control.token.v1",
|
|
891
|
+
"openclaw.control.settings.v1",
|
|
892
|
+
"openclaw.gateway.token",
|
|
893
|
+
"gateway.token",
|
|
894
|
+
"gatewayToken",
|
|
895
|
+
"token"
|
|
896
|
+
];
|
|
897
|
+
var i;
|
|
898
|
+
for (i = 0; i < ocKeys.length; i++) {
|
|
899
|
+
var tk = readTokenFromStorage(window.localStorage, ocKeys[i]);
|
|
900
|
+
if (tk) {
|
|
901
|
+
persistToken(tk);
|
|
902
|
+
return tk;
|
|
903
|
+
}
|
|
904
|
+
tk = readTokenFromStorage(window.sessionStorage, ocKeys[i]);
|
|
905
|
+
if (tk) {
|
|
906
|
+
persistToken(tk);
|
|
907
|
+
return tk;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
829
910
|
try {
|
|
830
|
-
var
|
|
831
|
-
|
|
911
|
+
var legacy = localStorage.getItem(LS_TOKEN_KEY);
|
|
912
|
+
legacy = legacy ? tryTokenFromJsonString(legacy) : "";
|
|
913
|
+
return legacy && String(legacy).trim() ? String(legacy).trim() : "";
|
|
832
914
|
} catch (e) {
|
|
833
915
|
return "";
|
|
834
916
|
}
|
|
@@ -855,7 +937,7 @@ function buildClientScript(): string {
|
|
|
855
937
|
if (isMemoryPanelLoopbackHost()) {
|
|
856
938
|
return (
|
|
857
939
|
" 解决办法:编辑 ~/.openclaw/openclaw.json,在 gateway.controlUi 中加入 \\"allowInsecureAuth\\": true,保存后执行 openclaw gateway restart,再刷新本页。" +
|
|
858
|
-
" 请用 http://127.0.0.1
|
|
940
|
+
" 请用 http://localhost 打开本机面板(从 127.0.0.1 访问会自动跳转);不要用局域网 IP,除非按下文配置。"
|
|
859
941
|
);
|
|
860
942
|
}
|
|
861
943
|
return (
|
|
@@ -1020,10 +1102,76 @@ function buildClientScript(): string {
|
|
|
1020
1102
|
});
|
|
1021
1103
|
}
|
|
1022
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* 非本机 hostname(公网 IP / 域名)时 Gateway WebSocket 会剥离 operator.admin;同源 HTTP 仅需 gateway token(与观测插件一致)。
|
|
1107
|
+
*/
|
|
1108
|
+
async function fetchMemoryApiHttp(method, params) {
|
|
1109
|
+
var tok = getGatewayToken();
|
|
1110
|
+
var headers = { "Content-Type": "application/json" };
|
|
1111
|
+
if (tok) {
|
|
1112
|
+
headers["Authorization"] = "Bearer " + tok;
|
|
1113
|
+
}
|
|
1114
|
+
var resp;
|
|
1115
|
+
try {
|
|
1116
|
+
resp = await fetch("/plugins/memory/api/v1/call", {
|
|
1117
|
+
method: "POST",
|
|
1118
|
+
headers: headers,
|
|
1119
|
+
body: JSON.stringify({ method: method, params: params || {} })
|
|
1120
|
+
});
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
return {
|
|
1123
|
+
ok: false,
|
|
1124
|
+
status: 0,
|
|
1125
|
+
json: function () {
|
|
1126
|
+
return Promise.resolve({ error: String(e) });
|
|
1127
|
+
},
|
|
1128
|
+
text: function () {
|
|
1129
|
+
return Promise.resolve(String(e));
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
var j;
|
|
1134
|
+
try {
|
|
1135
|
+
j = await resp.json();
|
|
1136
|
+
} catch (e2) {
|
|
1137
|
+
j = { error: "invalid JSON response" };
|
|
1138
|
+
}
|
|
1139
|
+
if (resp.ok) {
|
|
1140
|
+
return {
|
|
1141
|
+
ok: true,
|
|
1142
|
+
status: 200,
|
|
1143
|
+
json: function () {
|
|
1144
|
+
return Promise.resolve(j);
|
|
1145
|
+
},
|
|
1146
|
+
text: function () {
|
|
1147
|
+
return Promise.resolve("");
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
var st = resp.status;
|
|
1152
|
+
if (j && typeof j.status === "number" && Number.isFinite(j.status)) {
|
|
1153
|
+
st = j.status;
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
ok: false,
|
|
1157
|
+
status: st,
|
|
1158
|
+
json: function () {
|
|
1159
|
+
return Promise.resolve(j);
|
|
1160
|
+
},
|
|
1161
|
+
text: function () {
|
|
1162
|
+
return Promise.resolve(String((j && j.error) || resp.status));
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1023
1167
|
/** 与原先 fetch 类似的接口:ok / status / json() / text() */
|
|
1024
1168
|
async function fetchMemoryApi(method, params) {
|
|
1169
|
+
var p = params || {};
|
|
1170
|
+
if (!isMemoryPanelLoopbackHost()) {
|
|
1171
|
+
return fetchMemoryApiHttp(method, p);
|
|
1172
|
+
}
|
|
1025
1173
|
try {
|
|
1026
|
-
var res = await gatewayRpc(method,
|
|
1174
|
+
var res = await gatewayRpc(method, p);
|
|
1027
1175
|
if (res.ok) {
|
|
1028
1176
|
return {
|
|
1029
1177
|
ok: true,
|
|
@@ -1036,6 +1184,12 @@ function buildClientScript(): string {
|
|
|
1036
1184
|
}
|
|
1037
1185
|
};
|
|
1038
1186
|
}
|
|
1187
|
+
if (res.unauthorized) {
|
|
1188
|
+
var httpRes = await fetchMemoryApiHttp(method, p);
|
|
1189
|
+
if (httpRes.ok) {
|
|
1190
|
+
return httpRes;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1039
1193
|
var st = res.payload && res.payload.status ? parseInt(String(res.payload.status), 10) : 0;
|
|
1040
1194
|
if (!Number.isFinite(st) || st < 400) {
|
|
1041
1195
|
st = res.unauthorized ? 403 : 502;
|