@t0ken.ai/memoryx-openclaw-plugin 2.2.61 → 2.2.62

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
@@ -175,14 +175,14 @@ Memories are categorized by the server:
175
175
  **SlimClaw-style (recommended, no file changes):** Like SlimClaw, you can use a **virtual provider** so all traffic goes through the Sidecar without the plugin mutating config or files. Add this to `~/.openclaw/openclaw.json` under `models.providers`:
176
176
 
177
177
  ```json
178
- "memoryx-proxy": {
178
+ "memoryx-gateway": {
179
179
  "baseUrl": "http://localhost:3335/v1",
180
180
  "apiKey": "placeholder",
181
181
  "models": [{ "id": "auto" }]
182
182
  }
183
183
  ```
184
184
 
185
- Then add an auth profile for `memoryx-proxy` (e.g. in `~/.openclaw/agents/main/agent/auth-profiles.json` or global auth) with any placeholder key. In the UI, choose model **memoryx-proxy/auto**. Requests will go to the Sidecar, which forwards to the MemoryX server with your real provider credentials (you configure which provider/model to use via the plugin/Sidecar logic). The plugin does not modify your config or `models.json`; it only runs the Sidecar.
185
+ Then add an auth profile for `memoryx-gateway` (e.g. in `~/.openclaw/agents/main/agent/auth-profiles.json` or global auth) with any placeholder key. In the UI, choose model **MemoryX Gateway** (id: `memoryx-gateway/auto`). Requests will go to the Sidecar, which forwards to the MemoryX server with your real provider credentials (you configure which provider/model to use via the plugin/Sidecar logic). The plugin does not modify your config or `models.json`; it only runs the Sidecar.
186
186
 
187
187
  ## License
188
188
 
@@ -1,8 +1,10 @@
1
- export declare const PLUGIN_VERSION = "2.2.61";
1
+ export declare const PLUGIN_VERSION = "2.2.62";
2
2
  export declare const DEFAULT_API_BASE = "https://t0ken.ai/api";
3
3
  export declare const PLUGIN_DIR: string;
4
4
  /** 真实上游 baseUrl 缓存文件:重启后若配置已被改成 localhost,从此文件恢复各厂商真实地址。 */
5
5
  export declare const REAL_UPSTREAM_BASEURL_CACHE_FILE: string;
6
6
  /** Sidecar HTTP port — fixed so openclaw.json can point to localhost:37169. Do not use a range. */
7
7
  export declare const SIDECAR_PORT = 37169;
8
+ /** 单条 plugin.log 最大字节数,超过则轮转为 plugin.log.old 并重新写。 */
9
+ export declare const MAX_LOG_FILE_BYTES: number;
8
10
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,eAAO,MAAM,UAAU,QAAgF,CAAC;AAExG,4DAA4D;AAC5D,eAAO,MAAM,gCAAgC,QAAsD,CAAC;AAEpG,mGAAmG;AACnG,eAAO,MAAM,YAAY,QAAQ,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,eAAO,MAAM,UAAU,QAAgF,CAAC;AAExG,4DAA4D;AAC5D,eAAO,MAAM,gCAAgC,QAAsD,CAAC;AAEpG,mGAAmG;AACnG,eAAO,MAAM,YAAY,QAAQ,CAAC;AAElC,sDAAsD;AACtD,eAAO,MAAM,kBAAkB,QAAkB,CAAC"}
package/dist/constants.js CHANGED
@@ -4,10 +4,12 @@
4
4
  import * as path from "path";
5
5
  import * as os from "os";
6
6
  // Plugin version - synced from package.json by prebuild script
7
- export const PLUGIN_VERSION = "2.2.61";
7
+ export const PLUGIN_VERSION = "2.2.62";
8
8
  export const DEFAULT_API_BASE = "https://t0ken.ai/api";
9
9
  export const PLUGIN_DIR = path.join(os.homedir(), ".openclaw", "extensions", "memoryx-openclaw-plugin");
10
10
  /** 真实上游 baseUrl 缓存文件:重启后若配置已被改成 localhost,从此文件恢复各厂商真实地址。 */
11
11
  export const REAL_UPSTREAM_BASEURL_CACHE_FILE = path.join(PLUGIN_DIR, "real-upstream-baseurl.json");
12
12
  /** Sidecar HTTP port — fixed so openclaw.json can point to localhost:37169. Do not use a range. */
13
13
  export const SIDECAR_PORT = 37169;
14
+ /** 单条 plugin.log 最大字节数,超过则轮转为 plugin.log.old 并重新写。 */
15
+ export const MAX_LOG_FILE_BYTES = 2 * 1024 * 1024; // 2MB
package/dist/hooks.d.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  import type { MemoryXPlugin } from "./plugin-core.js";
5
5
  export declare function registerHooks(api: any, plugin: MemoryXPlugin, wrapProvidersWithProxy: () => void, applySidecarRedirect: (opts?: {
6
6
  quiet?: boolean;
7
- }) => void): void;
7
+ }) => void, shouldApplyProxy: () => boolean): void;
8
8
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,wBAAgB,aAAa,CACzB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,aAAa,EACrB,sBAAsB,EAAE,MAAM,IAAI,EAClC,oBAAoB,EAAE,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,GAC3D,IAAI,CAyCN"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,wBAAgB,aAAa,CACzB,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,aAAa,EACrB,sBAAsB,EAAE,MAAM,IAAI,EAClC,oBAAoB,EAAE,CAAC,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,EAC1D,gBAAgB,EAAE,MAAM,OAAO,GAChC,IAAI,CAsCN"}
package/dist/hooks.js CHANGED
@@ -1,4 +1,4 @@
1
- export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect) {
1
+ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy) {
2
2
  let useVirtualProviderInLastRun = false;
3
3
  api.on("message_received", async (event) => {
4
4
  const { content } = event;
@@ -6,27 +6,25 @@ export function registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarR
6
6
  await plugin.onMessage("user", content);
7
7
  });
8
8
  api.on("llm_input", (_event) => {
9
- useVirtualProviderInLastRun = _event?.provider === "memoryx-proxy";
9
+ useVirtualProviderInLastRun = _event?.provider === "memoryx-gateway";
10
10
  });
11
11
  api.on("llm_output", async (event) => {
12
- const { assistantTexts, provider } = event;
13
- if (assistantTexts && Array.isArray(assistantTexts) && plugin && provider !== "memoryx-proxy") {
12
+ const { assistantTexts } = event;
13
+ if (assistantTexts && Array.isArray(assistantTexts) && plugin) {
14
14
  const fullContent = assistantTexts.join("\n");
15
15
  if (fullContent && fullContent.length >= 2)
16
16
  await plugin.onMessage("assistant", fullContent);
17
17
  }
18
18
  });
19
19
  api.on("before_agent_start", async (event) => {
20
- wrapProvidersWithProxy();
21
- applySidecarRedirect({ quiet: true });
22
- const { prompt } = event;
23
- if (!prompt || prompt.length < 2 || !plugin)
24
- return;
20
+ if (shouldApplyProxy()) {
21
+ wrapProvidersWithProxy();
22
+ applySidecarRedirect({ quiet: true });
23
+ }
25
24
  try {
26
25
  await plugin.startTimersIfNeeded();
27
- await plugin.onMessage("user", prompt);
28
- // 记忆注入改由服务端 llm_proxy 在转发前注入到请求体(system/消息),不在此处用 prependContext,
29
- // 避免 OpenClaw 把 prependContext 当成单独用户消息写入历史(role=user 的 MemoryX Context 条)。
26
+ // 用户消息只由 message_received 写入一次,此处不再 onMessage("user", prompt)
27
+ // 避免与 message_received 重复,且 before_agent_start 可能被多次触发(如 tool 多轮)导致同条 prompt 重复入队。
30
28
  }
31
29
  catch (error) {
32
30
  api.logger.warn(`[MemoryX] before_agent_start failed: ${error}`);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAU,MAAM,kBAAkB,CAAC;;;;;;kBAyBvC,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AARzD,wBAkGE;AAEF,OAAO,EAAE,aAAa,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAU,MAAM,kBAAkB,CAAC;;;;;;kBAyBvC,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AARzD,wBAqGE;AAEF,OAAO,EAAE,aAAa,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -54,7 +54,9 @@ export default {
54
54
  });
55
55
  const getSidecarBase = () => `http://localhost:${sidecar.getPort()}`;
56
56
  const { applySidecarRedirect, wrapProvidersWithProxy } = createProxyRedirect(api, getSidecarBase, realProviderHeader);
57
- registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect);
57
+ /** 仅当用户当前默认使用的是 memoryx-gateway 时才做拦截/注入,否则不碰配置,避免装完插件就用不了 OpenClaw。 */
58
+ const shouldApplyProxy = () => getDefaultModelAndProvider(extractProviderCredentials(api.config), api.config).provider === "memoryx-gateway";
59
+ registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy);
58
60
  api.registerService({
59
61
  id: "memoryx-sidecar",
60
62
  start: async () => {
@@ -71,9 +73,14 @@ export default {
71
73
  api.logger.info("[MemoryX] Sidecar stopped");
72
74
  },
73
75
  });
74
- wrapProvidersWithProxy();
75
- applySidecarRedirect();
76
- api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Requests go through MemoryX proxy with your configured model.`);
76
+ if (defaultConfig.provider === "memoryx-gateway") {
77
+ wrapProvidersWithProxy();
78
+ applySidecarRedirect();
79
+ api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Default is MemoryX Gateway, redirect applied.`);
80
+ }
81
+ else {
82
+ api.logger.info(`[MemoryX] ✅ Plugin v${PLUGIN_VERSION} ready! Default is ${defaultConfig.provider}/${defaultConfig.model}; switch to MemoryX Gateway to use MemoryX.`);
83
+ }
77
84
  setImmediate(async () => {
78
85
  try {
79
86
  await plugin.init();
@@ -85,19 +92,17 @@ export default {
85
92
  catch (e) {
86
93
  // ignore
87
94
  }
95
+ // 仅用「非 localhost」的 baseUrl 更新缓存,避免配置被改成 localhost 时覆盖掉真实地址
88
96
  try {
89
97
  const cached = await loadRealUpstreamBaseUrlCache();
90
- const merged = new Map(realUpstreamBaseUrlMap);
91
- for (const [id, url] of cached) {
92
- const current = merged.get(id);
93
- if (current && isLocalhostBaseUrl(current, sidecarBaseUrl) && url) {
98
+ const merged = new Map(cached);
99
+ for (const [id, url] of realUpstreamBaseUrlMap) {
100
+ if (id === "memoryx-gateway")
101
+ continue;
102
+ if (url && !isLocalhostBaseUrl(url, sidecarBaseUrl)) {
94
103
  merged.set(id, url);
95
- log(`[Proxy] Restored ${id} baseUrl from cache (config was localhost)`);
96
104
  }
97
105
  }
98
- if (cached.size > 0 && merged.size > 0) {
99
- sidecar.updateRealUpstreamBaseUrlMap(merged);
100
- }
101
106
  await saveRealUpstreamBaseUrlCache(merged);
102
107
  }
103
108
  catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,QAAQ,QAAsC,CAAC;AAQ5D;;;GAGG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CA8B9E"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,QAAQ,QAAsC,CAAC;AA4B5D;;;GAGG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,IAAI,CAiD9E"}
package/dist/logger.js CHANGED
@@ -1,17 +1,44 @@
1
1
  /**
2
2
  * Layered logging: file always, optional console for important messages.
3
+ * Log file size is capped: when plugin.log exceeds MAX_LOG_FILE_BYTES it is rotated to plugin.log.old and a new file is started.
3
4
  */
4
5
  import * as fs from "fs";
5
6
  import * as path from "path";
6
- import { PLUGIN_DIR } from "./constants.js";
7
+ import { PLUGIN_DIR, MAX_LOG_FILE_BYTES } from "./constants.js";
7
8
  let logStream = null;
8
9
  let logStreamReady = false;
10
+ let writeCount = 0;
11
+ const CHECK_ROTATE_EVERY = 500;
9
12
  export const LOG_FILE = path.join(PLUGIN_DIR, "plugin.log");
13
+ const LOG_FILE_OLD = path.join(PLUGIN_DIR, "plugin.log.old");
10
14
  function ensureDir() {
11
15
  if (!fs.existsSync(PLUGIN_DIR)) {
12
16
  fs.mkdirSync(PLUGIN_DIR, { recursive: true });
13
17
  }
14
18
  }
19
+ function rotateIfNeeded() {
20
+ try {
21
+ if (!fs.existsSync(LOG_FILE))
22
+ return;
23
+ const stat = fs.statSync(LOG_FILE);
24
+ if (stat.size < MAX_LOG_FILE_BYTES)
25
+ return;
26
+ if (logStream) {
27
+ try {
28
+ logStream.end();
29
+ }
30
+ catch (_) { }
31
+ logStream = null;
32
+ logStreamReady = false;
33
+ }
34
+ if (fs.existsSync(LOG_FILE_OLD))
35
+ fs.unlinkSync(LOG_FILE_OLD);
36
+ fs.renameSync(LOG_FILE, LOG_FILE_OLD);
37
+ }
38
+ catch (_) {
39
+ /* ignore */
40
+ }
41
+ }
15
42
  /**
16
43
  * Layered logging: all logs go to file (stream when ready, else sync append so nothing is lost).
17
44
  * When console=true, also log to console (for important/guidance messages).
@@ -26,8 +53,28 @@ export function log(message, options = {}) {
26
53
  try {
27
54
  if (!logStreamReady) {
28
55
  ensureDir();
56
+ rotateIfNeeded();
29
57
  logStream = fs.createWriteStream(LOG_FILE, { flags: "a" });
30
58
  logStreamReady = true;
59
+ writeCount = 0;
60
+ }
61
+ writeCount++;
62
+ if (writeCount >= CHECK_ROTATE_EVERY) {
63
+ writeCount = 0;
64
+ try {
65
+ if (fs.existsSync(LOG_FILE)) {
66
+ const stat = fs.statSync(LOG_FILE);
67
+ if (stat.size >= MAX_LOG_FILE_BYTES) {
68
+ rotateIfNeeded();
69
+ if (!logStreamReady) {
70
+ ensureDir();
71
+ logStream = fs.createWriteStream(LOG_FILE, { flags: "a" });
72
+ logStreamReady = true;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ catch (_) { }
31
78
  }
32
79
  logStream?.write(line, (err) => {
33
80
  if (err) {
@@ -19,7 +19,7 @@ export declare function extractProviderCredentials(config: any, providerOverride
19
19
  * 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
20
20
  * - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
21
21
  * - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
22
- * 返回 Map<providerId, baseUrl>,memoryx-proxy 不写入。
22
+ * 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
23
23
  */
24
24
  export declare function buildRealUpstreamBaseUrlMap(credentials: Map<string, ProviderCredentials>, _sidecarBaseUrl: string): Map<string, string>;
25
25
  /**
@@ -56,9 +56,9 @@ export function extractProviderCredentials(config, providerOverrides) {
56
56
  process.env[`${id.toUpperCase()}_API_KEY`] ||
57
57
  authKeys.get(id) ||
58
58
  "";
59
- const isMemoryxProxy = (id === "memoryx-proxy");
59
+ const isMemoryxGateway = (id === "memoryx-gateway");
60
60
  credentials.set(id, {
61
- baseUrl: isMemoryxProxy ? "" : (override?.baseUrl || pc.baseUrl),
61
+ baseUrl: isMemoryxGateway ? "" : (override?.baseUrl || pc.baseUrl),
62
62
  apiKey,
63
63
  models: pc.models,
64
64
  });
@@ -72,12 +72,12 @@ export function extractProviderCredentials(config, providerOverrides) {
72
72
  * 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
73
73
  * - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
74
74
  * - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
75
- * 返回 Map<providerId, baseUrl>,memoryx-proxy 不写入。
75
+ * 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
76
76
  */
77
77
  export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
78
78
  const map = new Map();
79
79
  for (const [id, creds] of credentials) {
80
- if (id === "memoryx-proxy")
80
+ if (id === "memoryx-gateway")
81
81
  continue;
82
82
  const base = (creds.baseUrl ?? "").trim();
83
83
  map.set(id, base);
@@ -92,7 +92,7 @@ export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
92
92
  export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseUrlMap) {
93
93
  const out = new Map();
94
94
  for (const [id, creds] of credentials) {
95
- if (id === "memoryx-proxy") {
95
+ if (id === "memoryx-gateway") {
96
96
  out.set(id, creds);
97
97
  continue;
98
98
  }
@@ -129,7 +129,7 @@ export async function saveRealUpstreamBaseUrlCache(map) {
129
129
  await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
130
130
  const obj = {};
131
131
  for (const [id, url] of map) {
132
- if (id !== "memoryx-proxy" && url)
132
+ if (id !== "memoryx-gateway" && url)
133
133
  obj[id] = url;
134
134
  }
135
135
  await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
@@ -15,7 +15,7 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
15
15
  const providers = api.config.models.providers;
16
16
  let n = 0;
17
17
  for (const providerId of creds.keys()) {
18
- if (providerId === "memoryx-proxy")
18
+ if (providerId === "memoryx-gateway")
19
19
  continue;
20
20
  const p = providers[providerId];
21
21
  if (!p || typeof p !== "object")
@@ -33,7 +33,7 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
33
33
  }
34
34
  if (n > 0) {
35
35
  for (const providerId of creds.keys()) {
36
- if (providerId === "memoryx-proxy")
36
+ if (providerId === "memoryx-gateway")
37
37
  continue;
38
38
  const p = providers[providerId];
39
39
  const readBack = (p && typeof p === "object" && p.baseUrl) ? p.baseUrl : "<missing>";
@@ -52,7 +52,7 @@ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
52
52
  const handler = {
53
53
  get(target, prop) {
54
54
  const val = target[prop];
55
- if (val && typeof val === "object" && creds.has(prop) && prop !== "memoryx-proxy") {
55
+ if (val && typeof val === "object" && creds.has(prop) && prop !== "memoryx-gateway") {
56
56
  return new Proxy(val, {
57
57
  get(t, k) {
58
58
  if (k === "baseUrl")
package/dist/sidecar.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Local HTTP Sidecar: 仅转发到服务端 proxy(记忆注入),无本地回落。
3
+ */
1
4
  import type { ProviderCredentials } from "./types.js";
2
5
  import type { PluginConfig } from "./types.js";
3
6
  export type GetSDK = (config?: PluginConfig) => Promise<any>;
@@ -31,10 +34,8 @@ export declare class SidecarServer {
31
34
  private redactHeaders;
32
35
  /** Build forward request headers from apiKey in OpenClaw style; does not overwrite user config */
33
36
  private buildForwardHeaders;
34
- /** 1) Exclude memoryx-proxy, use real provider config; 2) On proxy error fall back to direct; on direct error return error to caller. */
37
+ /** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
35
38
  private handleRequest;
36
- private directRequest;
37
- private writeResponse;
38
39
  private readBody;
39
40
  }
40
41
  //# sourceMappingURL=sidecar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,OAAO,CAAiB;gBAG5B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,EAC9D,OAAO,EAAE,cAAc;IAQrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,IAAI,MAAM;IAMjB;;OAEG;IACH,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAU5D,+EAA+E;IAC/E,OAAO,CAAC,aAAa;IAUrB,kGAAkG;IAClG,OAAO,CAAC,mBAAmB;IAa3B,yIAAyI;YAC3H,aAAa;YAoLb,aAAa;YAwBb,aAAa;IAqB3B,OAAO,CAAC,QAAQ;CAQnB"}
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,OAAO,CAAiB;gBAG5B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,EAC9D,OAAO,EAAE,cAAc;IAQrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,IAAI,MAAM;IAMjB;;OAEG;IACH,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAU5D,+EAA+E;IAC/E,OAAO,CAAC,aAAa;IAUrB,kGAAkG;IAClG,OAAO,CAAC,mBAAmB;IAa3B,iCAAiC;YACnB,aAAa;IAmN3B,OAAO,CAAC,QAAQ;CAQnB"}
package/dist/sidecar.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Local HTTP Sidecar: receives OpenClaw requests, forwards to MemoryX proxy or falls back to direct provider.
2
+ * Local HTTP Sidecar: 仅转发到服务端 proxy(记忆注入),无本地回落。
3
3
  */
4
4
  import * as http from "http";
5
5
  import { SIDECAR_PORT } from "./constants.js";
@@ -95,7 +95,7 @@ export class SidecarServer {
95
95
  }
96
96
  return h;
97
97
  }
98
- /** 1) Exclude memoryx-proxy, use real provider config; 2) On proxy error fall back to direct; on direct error return error to caller. */
98
+ /** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
99
99
  async handleRequest(req, res) {
100
100
  const url = req.url || "/";
101
101
  const method = req.method?.toUpperCase();
@@ -123,11 +123,22 @@ export class SidecarServer {
123
123
  catch {
124
124
  openaiRequest = {};
125
125
  }
126
+ const messages = openaiRequest.messages || [];
127
+ const roleSeq = messages.map((m) => (m?.role || "?").slice(0, 1)).join("");
128
+ const lastMsg = messages.length ? messages[messages.length - 1] : null;
129
+ const lastRole = lastMsg ? (lastMsg.role || "?") : "-";
130
+ const lastContentLen = lastMsg && lastMsg.content != null
131
+ ? typeof lastMsg.content === "string"
132
+ ? lastMsg.content.length
133
+ : JSON.stringify(lastMsg.content).length
134
+ : 0;
135
+ const topKeys = Object.keys(openaiRequest).filter((k) => !["messages", "model"].includes(k) && openaiRequest[k] != null);
136
+ log(`[Sidecar] ${reqId} request detail: bodyLen=${(body || "").length} messages=${messages.length} roles=${roleSeq} lastRole=${lastRole} lastContentLen=${lastContentLen} model=${openaiRequest.model ?? "-"} stream=${!!openaiRequest.stream} extraKeys=[${topKeys.join(",")}]`);
126
137
  const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
127
138
  const modelStr = openaiRequest.model || "";
128
139
  let provider = rawProvider || (modelStr.includes("/") ? modelStr.split("/")[0] : "") || "";
129
- if (provider === "memoryx-proxy") {
130
- const real = this.availableProviders.find((p) => p.provider !== "memoryx-proxy");
140
+ if (provider === "memoryx-gateway") {
141
+ const real = this.availableProviders.find((p) => p.provider !== "memoryx-gateway");
131
142
  provider = real ? real.provider : "";
132
143
  }
133
144
  if (!provider || !this.credentials.has(provider)) {
@@ -135,7 +146,7 @@ export class SidecarServer {
135
146
  }
136
147
  if (!provider || !this.credentials.has(provider)) {
137
148
  for (const [id] of this.credentials) {
138
- if (id !== "memoryx-proxy") {
149
+ if (id !== "memoryx-gateway") {
139
150
  provider = id;
140
151
  break;
141
152
  }
@@ -144,7 +155,7 @@ export class SidecarServer {
144
155
  let creds = provider ? this.credentials.get(provider) : undefined;
145
156
  if (!creds?.baseUrl?.trim() || !(creds.apiKey ?? "").trim()) {
146
157
  for (const [id, c] of this.credentials) {
147
- if (id !== "memoryx-proxy" && (c.baseUrl || "").trim() && (c.apiKey ?? "").trim()) {
158
+ if (id !== "memoryx-gateway" && (c.baseUrl || "").trim() && (c.apiKey ?? "").trim()) {
148
159
  provider = id;
149
160
  creds = c;
150
161
  break;
@@ -162,7 +173,7 @@ export class SidecarServer {
162
173
  const pathFromOpenClaw = (url.split("?")[0] || "/").trim() || "/";
163
174
  const targetUrl = base + (pathFromOpenClaw.startsWith("/") ? pathFromOpenClaw : "/" + pathFromOpenClaw);
164
175
  const currentModel = openaiRequest.model || "";
165
- if (!currentModel || currentModel === "auto" || currentModel.startsWith("memoryx-proxy/")) {
176
+ if (!currentModel || currentModel === "auto" || currentModel.startsWith("memoryx-gateway/")) {
166
177
  const firstId = creds?.models?.[0]?.id || creds?.models?.[0]?.name;
167
178
  if (firstId)
168
179
  openaiRequest.model = firstId;
@@ -176,14 +187,14 @@ export class SidecarServer {
176
187
  agentId = accountInfo.agentId || "";
177
188
  }
178
189
  catch (_e) {
179
- /* Do not block; proxy will fail and then fall back to direct */
190
+ /* Do not block; proxy may still accept request */
180
191
  }
181
192
  const headers = this.buildForwardHeaders(provider, apiKey);
182
- const messages = openaiRequest.messages || [];
183
193
  const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
184
194
  const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
185
195
  const proxyRequestBody = {
186
196
  targetUrl,
197
+ api_base: base,
187
198
  headers,
188
199
  body: openaiRequest,
189
200
  searchQuery,
@@ -212,9 +223,24 @@ export class SidecarServer {
212
223
  }
213
224
  const proxyFailed = !proxyResponse || proxyResponse.status >= 500;
214
225
  if (proxyFailed) {
215
- log(`[Sidecar] ${reqId} Proxy failed, fallback to direct POST ${targetUrl}`, { console: true });
216
- const directResponse = await this.directRequest(reqId, targetUrl, headers, openaiRequest);
217
- this.writeResponse(res, directResponse);
226
+ if (proxyResponse?.body) {
227
+ try {
228
+ if (typeof proxyResponse.body.cancel === "function") {
229
+ await proxyResponse.body.cancel();
230
+ }
231
+ else {
232
+ await proxyResponse.text();
233
+ }
234
+ }
235
+ catch (_) {
236
+ /* drain failed, ignore */
237
+ }
238
+ }
239
+ log(`[Sidecar] ${reqId} Proxy failed, returning error (no local fallback)`, { console: true });
240
+ const status = proxyResponse?.status ?? 502;
241
+ const detail = proxyResponse?.statusText || "MemoryX proxy unavailable";
242
+ res.writeHead(status, { "Content-Type": "application/json" });
243
+ res.end(JSON.stringify({ error: detail }));
218
244
  return;
219
245
  }
220
246
  const proxy = proxyResponse;
@@ -229,11 +255,11 @@ export class SidecarServer {
229
255
  }
230
256
  catch (firstErr) {
231
257
  reader.releaseLock();
232
- log(`[Sidecar] ${reqId} Proxy stream first chunk failed: ${firstErr?.message ?? "timeout"}, fallback to direct ${targetUrl}`, { console: true });
233
- const directResponse = await this.directRequest(reqId, targetUrl, headers, openaiRequest);
234
- if (directResponse)
235
- log(`[Sidecar] ${reqId} Direct response status=${directResponse.status}`);
236
- this.writeResponse(res, directResponse);
258
+ log(`[Sidecar] ${reqId} Proxy stream first chunk failed: ${firstErr?.message ?? "timeout"}`, {
259
+ console: true,
260
+ });
261
+ res.writeHead(502, { "Content-Type": "application/json" });
262
+ res.end(JSON.stringify({ error: "MemoryX proxy stream failed" }));
237
263
  return;
238
264
  }
239
265
  res.writeHead(proxy.status, {
@@ -270,47 +296,6 @@ export class SidecarServer {
270
296
  });
271
297
  res.end(text);
272
298
  }
273
- async directRequest(reqId, targetUrl, headers, openaiRequest) {
274
- log(`[Sidecar] ${reqId} Direct POST ${targetUrl} headers=${JSON.stringify(this.redactHeaders(headers))} model=${openaiRequest?.model} stream=${!!openaiRequest?.stream}`);
275
- try {
276
- const resp = await fetch(targetUrl, {
277
- method: "POST",
278
- headers,
279
- body: JSON.stringify(openaiRequest),
280
- });
281
- log(`[Sidecar] ${reqId} Direct response status=${resp.status} ${resp.statusText}`);
282
- return resp;
283
- }
284
- catch (e) {
285
- log(`[Sidecar] ${reqId} Direct fetch error: ${e?.message ?? String(e)}`, { console: true });
286
- return new Response(JSON.stringify({ error: e?.message || "Direct request failed" }), {
287
- status: 502,
288
- headers: { "Content-Type": "application/json" },
289
- });
290
- }
291
- }
292
- async writeResponse(res, directResponse) {
293
- const contentType = directResponse.headers.get("content-type") || "application/json";
294
- res.writeHead(directResponse.status, { "Content-Type": contentType });
295
- const body = directResponse.body;
296
- if (!body) {
297
- res.end();
298
- return;
299
- }
300
- const reader = body.getReader();
301
- try {
302
- while (true) {
303
- const { done, value } = await reader.read();
304
- if (done)
305
- break;
306
- res.write(Buffer.from(value));
307
- }
308
- }
309
- finally {
310
- reader.releaseLock();
311
- }
312
- res.end();
313
- }
314
299
  readBody(req) {
315
300
  return new Promise((resolve, reject) => {
316
301
  const chunks = [];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 与服务端 llm_proxy 一致的厂商请求体规范化。
3
+ * 部分 LLM 厂商非标准 OpenAI 格式,需在此做兼容后再转发;服务端用 LiteLLM,Sidecar 直连时用本模块保证同一套逻辑。
4
+ */
5
+ /**
6
+ * 判断是否为 Z.AI GLM-5:按 provider 或 targetUrl 与 model 判断。
7
+ */
8
+ export declare function isZaiGlm5(provider: string, targetUrl: string, model: string): boolean;
9
+ /**
10
+ * 按厂商与 URL/model 规范化请求体;与服务端逻辑一致,供 Sidecar 直连时使用。
11
+ * 返回规范化后的 body(不修改入参)。
12
+ */
13
+ export declare function normalizeBodyForVendor(provider: string, targetUrl: string, body: Record<string, unknown>): Record<string, unknown>;
14
+ //# sourceMappingURL=vendor-normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vendor-normalize.d.ts","sourceRoot":"","sources":["../src/vendor-normalize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAQrF;AA4DD;;;GAGG;AACH,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 与服务端 llm_proxy 一致的厂商请求体规范化。
3
+ * 部分 LLM 厂商非标准 OpenAI 格式,需在此做兼容后再转发;服务端用 LiteLLM,Sidecar 直连时用本模块保证同一套逻辑。
4
+ */
5
+ /**
6
+ * 判断是否为 Z.AI GLM-5:按 provider 或 targetUrl 与 model 判断。
7
+ */
8
+ export function isZaiGlm5(provider, targetUrl, model) {
9
+ const prov = (provider || "").toLowerCase();
10
+ const url = (targetUrl || "").toLowerCase();
11
+ const m = (model || "").toLowerCase();
12
+ if (prov === "zai" && (m === "glm-5" || m.endsWith("/glm-5")))
13
+ return true;
14
+ if ((url.includes("open.bigmodel.cn") || url.includes("api.z.ai")) && (m === "glm-5" || m.endsWith("/glm-5")))
15
+ return true;
16
+ return false;
17
+ }
18
+ /**
19
+ * Z.AI GLM-5 兼容:与 api/app/routers/llm_proxy.py _normalize_body_for_zai_glm5 保持一致。
20
+ * - messages 仅保留 role + content,role 仅允许 system/user/assistant
21
+ * - 顶层 system 并入 messages 首条 role=system
22
+ * - OpenClaw 的 developer 视为 system,其他非法 role 归为 user
23
+ * - 可选 thinking:未传时默认 {"type": "enabled"}
24
+ */
25
+ function normalizeBodyForZaiGlm5(body) {
26
+ const out = { ...body };
27
+ delete out.system;
28
+ const rawMessages = Array.isArray(out.messages) ? [...out.messages] : [];
29
+ let systemContent = (typeof body.system === "string" ? body.system : "").trim();
30
+ if (systemContent) {
31
+ const existingSystem = rawMessages.find((m) => (String(m?.role || "").toLowerCase() === "system"));
32
+ if (existingSystem && typeof existingSystem.content === "string") {
33
+ systemContent = `${existingSystem.content}\n\n${systemContent}`.trim();
34
+ }
35
+ }
36
+ const withoutSystem = rawMessages.filter((m) => String(m?.role || "").toLowerCase() !== "system");
37
+ const ROLE_MAP = { developer: "system" };
38
+ const messages = [];
39
+ for (const m of withoutSystem) {
40
+ let r = String(m.role || "").trim().toLowerCase();
41
+ if (r === "system" || r === "user" || r === "assistant") {
42
+ // keep
43
+ }
44
+ else if (ROLE_MAP[r]) {
45
+ r = ROLE_MAP[r];
46
+ }
47
+ else {
48
+ r = "user";
49
+ }
50
+ let content = m.content;
51
+ if (content == null)
52
+ content = "";
53
+ if (Array.isArray(content)) {
54
+ content = content
55
+ .map((part) => typeof part === "object" && part != null && "text" in part
56
+ ? String(part.text)
57
+ : String(part))
58
+ .join("\n");
59
+ }
60
+ const contentStr = String(content).trim();
61
+ messages.push({ role: r, content: contentStr || " " });
62
+ }
63
+ if (systemContent) {
64
+ messages.unshift({ role: "system", content: systemContent });
65
+ }
66
+ out.messages = messages;
67
+ if (!("thinking" in out) || out.thinking == null) {
68
+ out.thinking = { type: "enabled" };
69
+ }
70
+ return out;
71
+ }
72
+ /**
73
+ * 按厂商与 URL/model 规范化请求体;与服务端逻辑一致,供 Sidecar 直连时使用。
74
+ * 返回规范化后的 body(不修改入参)。
75
+ */
76
+ export function normalizeBodyForVendor(provider, targetUrl, body) {
77
+ const model = String(body?.model || "").trim();
78
+ if (isZaiGlm5(provider, targetUrl, model)) {
79
+ return normalizeBodyForZaiGlm5(body);
80
+ }
81
+ return body;
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t0ken.ai/memoryx-openclaw-plugin",
3
- "version": "2.2.61",
3
+ "version": "2.2.62",
4
4
  "description": "MemoryX real-time memory capture and recall plugin for OpenClaw (powered by @t0ken.ai/memoryx-sdk)",
5
5
  "type": "module",
6
6
  "author": "MemoryX Team",
@@ -15,7 +15,7 @@
15
15
  "prebuild": "node scripts/update-version.cjs",
16
16
  "build": "tsc",
17
17
  "test:recall": "node scripts/test-recall-capture.mjs",
18
- "reset-proxy-usage": "node scripts/reset-memoryx-proxy-usage.mjs"
18
+ "reset-gateway-usage": "node scripts/reset-memoryx-gateway-usage.mjs"
19
19
  },
20
20
  "keywords": [
21
21
  "openclaw",