@t0ken.ai/memoryx-openclaw-plugin 2.2.61 → 2.2.63
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 +2 -2
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +11 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -20
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +48 -1
- package/dist/proxy-credentials.d.ts +11 -1
- package/dist/proxy-credentials.d.ts.map +1 -1
- package/dist/proxy-credentials.js +54 -10
- package/dist/proxy-redirect.js +3 -3
- package/dist/sidecar.d.ts +4 -7
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +84 -88
- package/dist/vendor-normalize.d.ts +14 -0
- package/dist/vendor-normalize.d.ts.map +1 -0
- package/dist/vendor-normalize.js +82 -0
- package/package.json +2 -2
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-
|
|
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-
|
|
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
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "2.2.
|
|
1
|
+
export declare const PLUGIN_VERSION = "2.2.63";
|
|
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
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -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.
|
|
7
|
+
export const PLUGIN_VERSION = "2.2.63";
|
|
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, syncRealUpstreamBaseUrlCache?: () => Promise<void>): void;
|
|
8
8
|
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -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,
|
|
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,EAC/B,4BAA4B,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACnD,IAAI,CAuCN"}
|
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, syncRealUpstreamBaseUrlCache) {
|
|
2
2
|
let useVirtualProviderInLastRun = false;
|
|
3
3
|
api.on("message_received", async (event) => {
|
|
4
4
|
const { content } = event;
|
|
@@ -6,27 +6,26 @@ 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-
|
|
9
|
+
useVirtualProviderInLastRun = _event?.provider === "memoryx-gateway";
|
|
10
10
|
});
|
|
11
11
|
api.on("llm_output", async (event) => {
|
|
12
|
-
const { assistantTexts
|
|
13
|
-
if (assistantTexts && Array.isArray(assistantTexts) && plugin
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
if (shouldApplyProxy()) {
|
|
21
|
+
await syncRealUpstreamBaseUrlCache?.();
|
|
22
|
+
wrapProvidersWithProxy();
|
|
23
|
+
applySidecarRedirect({ quiet: true });
|
|
24
|
+
}
|
|
25
25
|
try {
|
|
26
26
|
await plugin.startTimersIfNeeded();
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
// 避免 OpenClaw 把 prependContext 当成单独用户消息写入历史(role=user 的 MemoryX Context 条)。
|
|
27
|
+
// 用户消息只由 message_received 写入一次,此处不再 onMessage("user", prompt),
|
|
28
|
+
// 避免与 message_received 重复,且 before_agent_start 可能被多次触发(如 tool 多轮)导致同条 prompt 重复入队。
|
|
30
29
|
}
|
|
31
30
|
catch (error) {
|
|
32
31
|
api.logger.warn(`[MemoryX] before_agent_start failed: ${error}`);
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;;;;;;
|
|
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;;;;;;kBAsBvC,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AARzD,wBA6FE;AAEF,OAAO,EAAE,aAAa,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { PLUGIN_VERSION, DEFAULT_API_BASE, SIDECAR_PORT } from "./constants.js";
|
|
17
17
|
import { log, LOG_FILE } from "./logger.js";
|
|
18
18
|
import { MemoryXPlugin, getSDK } from "./plugin-core.js";
|
|
19
|
-
import { extractProviderCredentials, getDefaultModelAndProvider,
|
|
19
|
+
import { extractProviderCredentials, getDefaultModelAndProvider, realUpstreamCredentialsForSidecar, syncRealUpstreamBaseUrlCacheFromConfig, } from "./proxy-credentials.js";
|
|
20
20
|
import { createProxyRedirect } from "./proxy-redirect.js";
|
|
21
21
|
import { SidecarServer } from "./sidecar.js";
|
|
22
22
|
import { registerHooks } from "./hooks.js";
|
|
@@ -43,8 +43,7 @@ export default {
|
|
|
43
43
|
log(`[Proxy] Default: ${defaultConfig.provider}/${defaultConfig.model}`);
|
|
44
44
|
const realProviderHeader = "X-MemoryX-Real-Provider";
|
|
45
45
|
const sidecarBaseUrl = `http://localhost:${SIDECAR_PORT}`;
|
|
46
|
-
const
|
|
47
|
-
const realUpstreamCredentials = realUpstreamCredentialsForSidecar(providerCredentials, realUpstreamBaseUrlMap);
|
|
46
|
+
const realUpstreamCredentials = realUpstreamCredentialsForSidecar(providerCredentials, new Map());
|
|
48
47
|
const proxyUrl = (pluginConfig?.apiBaseUrl || DEFAULT_API_BASE) + "/llm/proxy/chat/completions";
|
|
49
48
|
const sidecar = new SidecarServer(realUpstreamCredentials, { model: defaultConfig.model, provider: defaultConfig.provider }, defaultConfig.availableProviders, {
|
|
50
49
|
proxyUrl,
|
|
@@ -54,7 +53,10 @@ export default {
|
|
|
54
53
|
});
|
|
55
54
|
const getSidecarBase = () => `http://localhost:${sidecar.getPort()}`;
|
|
56
55
|
const { applySidecarRedirect, wrapProvidersWithProxy } = createProxyRedirect(api, getSidecarBase, realProviderHeader);
|
|
57
|
-
|
|
56
|
+
/** 仅当用户当前默认使用的是 memoryx-gateway 时才做拦截/注入,否则不碰配置,避免装完插件就用不了 OpenClaw。 */
|
|
57
|
+
const shouldApplyProxy = () => getDefaultModelAndProvider(extractProviderCredentials(api.config), api.config).provider === "memoryx-gateway";
|
|
58
|
+
const syncRealUpstreamBaseUrlCache = () => syncRealUpstreamBaseUrlCacheFromConfig(api.config, getSidecarBase());
|
|
59
|
+
registerHooks(api, plugin, wrapProvidersWithProxy, applySidecarRedirect, shouldApplyProxy, syncRealUpstreamBaseUrlCache);
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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();
|
|
@@ -86,19 +93,7 @@ export default {
|
|
|
86
93
|
// ignore
|
|
87
94
|
}
|
|
88
95
|
try {
|
|
89
|
-
|
|
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) {
|
|
94
|
-
merged.set(id, url);
|
|
95
|
-
log(`[Proxy] Restored ${id} baseUrl from cache (config was localhost)`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (cached.size > 0 && merged.size > 0) {
|
|
99
|
-
sidecar.updateRealUpstreamBaseUrlMap(merged);
|
|
100
|
-
}
|
|
101
|
-
await saveRealUpstreamBaseUrlCache(merged);
|
|
96
|
+
await syncRealUpstreamBaseUrlCacheFromConfig(api.config, sidecarBaseUrl);
|
|
102
97
|
}
|
|
103
98
|
catch (e) {
|
|
104
99
|
// ignore
|
package/dist/logger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"
|
|
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-
|
|
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
|
/**
|
|
@@ -30,8 +30,18 @@ export declare function buildRealUpstreamBaseUrlMap(credentials: Map<string, Pro
|
|
|
30
30
|
export declare function realUpstreamCredentialsForSidecar(credentials: Map<string, ProviderCredentials>, realUpstreamBaseUrlMap: Map<string, string>): Map<string, ProviderCredentials>;
|
|
31
31
|
/**
|
|
32
32
|
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
|
|
33
|
+
* 供插件 sync 时使用(先读已有缓存,没有则当空)。
|
|
33
34
|
*/
|
|
34
35
|
export declare function loadRealUpstreamBaseUrlCache(): Promise<Map<string, string>>;
|
|
36
|
+
/**
|
|
37
|
+
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射;文件不存在、IO 或解析错误时 throw。
|
|
38
|
+
* 供 Sidecar 每次请求读盘使用,不缓存。
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadRealUpstreamBaseUrlCacheStrict(): Promise<Map<string, string>>;
|
|
41
|
+
/**
|
|
42
|
+
* 根据 OpenClaw 配置更新 real-upstream-baseurl.json:非 localhost 的 baseUrl 写入/覆盖,localhost 绝不覆盖。
|
|
43
|
+
*/
|
|
44
|
+
export declare function syncRealUpstreamBaseUrlCacheFromConfig(config: any, sidecarBaseUrl: string): Promise<void>;
|
|
35
45
|
/**
|
|
36
46
|
* 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
|
|
37
47
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAuCtD,wBAAgB,0BAA0B,CACtC,MAAM,EAAE,GAAG,EACX,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9F,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,
|
|
1
|
+
{"version":3,"file":"proxy-credentials.d.ts","sourceRoot":"","sources":["../src/proxy-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAuCtD,wBAAgB,0BAA0B,CACtC,MAAM,EAAE,GAAG,EACX,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9F,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAiDlC;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE,MAAM,GACxB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQrB;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC7C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5C,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAWlC;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcjF;AAED;;;GAGG;AACH,wBAAsB,kCAAkC,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAUvF;AAED;;GAEG;AACH,wBAAsB,sCAAsC,CACxD,MAAM,EAAE,GAAG,EACX,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB1F;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAI/E;AAOD,wGAAwG;AACxG,wBAAgB,qBAAqB,CACjC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAC9C,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAc5C;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACvC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAC9C,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAU5C;AAoBD,+HAA+H;AAC/H,wBAAgB,0BAA0B,CACtC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,MAAM,CAAC,EAAE,GAAG,GACb;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CA8BrG"}
|
|
@@ -35,9 +35,6 @@ function loadAuthProfilesKeys(profilesPath) {
|
|
|
35
35
|
export function extractProviderCredentials(config, providerOverrides) {
|
|
36
36
|
const credentials = new Map();
|
|
37
37
|
const authKeys = loadAuthProfilesKeys(DEFAULT_AUTH_PROFILES_PATH);
|
|
38
|
-
if (authKeys.size > 0) {
|
|
39
|
-
log(`[Proxy] Loaded apiKey from auth-profiles for providers: ${[...authKeys.keys()].join(", ")}`);
|
|
40
|
-
}
|
|
41
38
|
if (config?.models?.providers) {
|
|
42
39
|
for (const [id, providerConfig] of Object.entries(config.models.providers)) {
|
|
43
40
|
if (typeof providerConfig !== "object" ||
|
|
@@ -56,28 +53,35 @@ export function extractProviderCredentials(config, providerOverrides) {
|
|
|
56
53
|
process.env[`${id.toUpperCase()}_API_KEY`] ||
|
|
57
54
|
authKeys.get(id) ||
|
|
58
55
|
"";
|
|
59
|
-
const
|
|
56
|
+
const isMemoryxGateway = (id === "memoryx-gateway");
|
|
60
57
|
credentials.set(id, {
|
|
61
|
-
baseUrl:
|
|
58
|
+
baseUrl: isMemoryxGateway ? "" : (override?.baseUrl || pc.baseUrl),
|
|
62
59
|
apiKey,
|
|
63
60
|
models: pc.models,
|
|
64
61
|
});
|
|
65
|
-
log(`[Proxy] Extracted credentials for ${id}: models=${pc.models?.length || 0}`);
|
|
66
62
|
}
|
|
67
63
|
}
|
|
68
64
|
}
|
|
65
|
+
if (credentials.size > 0) {
|
|
66
|
+
const parts = [...credentials.entries()].map(([id, c]) => `${id}(${c.models?.length ?? 0})`);
|
|
67
|
+
const authFromProfiles = [...credentials.keys()].filter((id) => authKeys.has(id));
|
|
68
|
+
const summary = authFromProfiles.length > 0
|
|
69
|
+
? `[Proxy] Credentials: ${parts.join(", ")}; apiKey from auth-profiles: ${authFromProfiles.join(", ")}`
|
|
70
|
+
: `[Proxy] Credentials: ${parts.join(", ")}`;
|
|
71
|
+
log(summary);
|
|
72
|
+
}
|
|
69
73
|
return credentials;
|
|
70
74
|
}
|
|
71
75
|
/**
|
|
72
76
|
* 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
|
|
73
77
|
* - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
|
|
74
78
|
* - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
|
|
75
|
-
* 返回 Map<providerId, baseUrl>,memoryx-
|
|
79
|
+
* 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
|
|
76
80
|
*/
|
|
77
81
|
export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
|
|
78
82
|
const map = new Map();
|
|
79
83
|
for (const [id, creds] of credentials) {
|
|
80
|
-
if (id === "memoryx-
|
|
84
|
+
if (id === "memoryx-gateway")
|
|
81
85
|
continue;
|
|
82
86
|
const base = (creds.baseUrl ?? "").trim();
|
|
83
87
|
map.set(id, base);
|
|
@@ -92,7 +96,7 @@ export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
|
|
|
92
96
|
export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseUrlMap) {
|
|
93
97
|
const out = new Map();
|
|
94
98
|
for (const [id, creds] of credentials) {
|
|
95
|
-
if (id === "memoryx-
|
|
99
|
+
if (id === "memoryx-gateway") {
|
|
96
100
|
out.set(id, creds);
|
|
97
101
|
continue;
|
|
98
102
|
}
|
|
@@ -103,6 +107,7 @@ export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseU
|
|
|
103
107
|
}
|
|
104
108
|
/**
|
|
105
109
|
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
|
|
110
|
+
* 供插件 sync 时使用(先读已有缓存,没有则当空)。
|
|
106
111
|
*/
|
|
107
112
|
export async function loadRealUpstreamBaseUrlCache() {
|
|
108
113
|
try {
|
|
@@ -121,6 +126,45 @@ export async function loadRealUpstreamBaseUrlCache() {
|
|
|
121
126
|
return new Map();
|
|
122
127
|
}
|
|
123
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 从缓存文件加载「厂商 -> 真实 baseUrl」映射;文件不存在、IO 或解析错误时 throw。
|
|
131
|
+
* 供 Sidecar 每次请求读盘使用,不缓存。
|
|
132
|
+
*/
|
|
133
|
+
export async function loadRealUpstreamBaseUrlCacheStrict() {
|
|
134
|
+
const raw = await fs.promises.readFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, "utf8");
|
|
135
|
+
const obj = JSON.parse(raw);
|
|
136
|
+
const map = new Map();
|
|
137
|
+
if (obj && typeof obj === "object") {
|
|
138
|
+
for (const [id, url] of Object.entries(obj)) {
|
|
139
|
+
if (id && typeof url === "string" && url.trim())
|
|
140
|
+
map.set(id, url.trim());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return map;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 根据 OpenClaw 配置更新 real-upstream-baseurl.json:非 localhost 的 baseUrl 写入/覆盖,localhost 绝不覆盖。
|
|
147
|
+
*/
|
|
148
|
+
export async function syncRealUpstreamBaseUrlCacheFromConfig(config, sidecarBaseUrl) {
|
|
149
|
+
let cached;
|
|
150
|
+
try {
|
|
151
|
+
cached = await loadRealUpstreamBaseUrlCache();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
cached = new Map();
|
|
155
|
+
}
|
|
156
|
+
const credentials = extractProviderCredentials(config);
|
|
157
|
+
const fromConfig = buildRealUpstreamBaseUrlMap(credentials, sidecarBaseUrl);
|
|
158
|
+
const merged = new Map(cached);
|
|
159
|
+
for (const [id, url] of fromConfig) {
|
|
160
|
+
if (id === "memoryx-gateway")
|
|
161
|
+
continue;
|
|
162
|
+
if (url && !isLocalhostBaseUrl(url, sidecarBaseUrl)) {
|
|
163
|
+
merged.set(id, url);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
await saveRealUpstreamBaseUrlCache(merged);
|
|
167
|
+
}
|
|
124
168
|
/**
|
|
125
169
|
* 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
|
|
126
170
|
*/
|
|
@@ -129,7 +173,7 @@ export async function saveRealUpstreamBaseUrlCache(map) {
|
|
|
129
173
|
await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
|
|
130
174
|
const obj = {};
|
|
131
175
|
for (const [id, url] of map) {
|
|
132
|
-
if (id !== "memoryx-
|
|
176
|
+
if (id !== "memoryx-gateway" && url)
|
|
133
177
|
obj[id] = url;
|
|
134
178
|
}
|
|
135
179
|
await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
|
package/dist/proxy-redirect.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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>;
|
|
@@ -23,18 +26,12 @@ export declare class SidecarServer {
|
|
|
23
26
|
start(): Promise<void>;
|
|
24
27
|
stop(): Promise<void>;
|
|
25
28
|
getPort(): number;
|
|
26
|
-
/**
|
|
27
|
-
* 用持久化/合并后的真实上游 baseUrl 映射更新 credentials,避免重启后配置已是 localhost 时仍用旧缓存。
|
|
28
|
-
*/
|
|
29
|
-
updateRealUpstreamBaseUrlMap(map: Map<string, string>): void;
|
|
30
29
|
/** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
|
|
31
30
|
private redactHeaders;
|
|
32
31
|
/** Build forward request headers from apiKey in OpenClaw style; does not overwrite user config */
|
|
33
32
|
private buildForwardHeaders;
|
|
34
|
-
/**
|
|
33
|
+
/** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
|
|
35
34
|
private handleRequest;
|
|
36
|
-
private directRequest;
|
|
37
|
-
private writeResponse;
|
|
38
35
|
private readBody;
|
|
39
36
|
}
|
|
40
37
|
//# sourceMappingURL=sidecar.d.ts.map
|
package/dist/sidecar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"
|
|
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;AAK/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,+EAA+E;IAC/E,OAAO,CAAC,aAAa;IAUrB,kGAAkG;IAClG,OAAO,CAAC,mBAAmB;IAa3B,iCAAiC;YACnB,aAAa;IA6O3B,OAAO,CAAC,QAAQ;CAQnB"}
|
package/dist/sidecar.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local HTTP Sidecar:
|
|
2
|
+
* Local HTTP Sidecar: 仅转发到服务端 proxy(记忆注入),无本地回落。
|
|
3
3
|
*/
|
|
4
4
|
import * as http from "http";
|
|
5
5
|
import { SIDECAR_PORT } from "./constants.js";
|
|
6
6
|
import { log, LOG_FILE } from "./logger.js";
|
|
7
|
+
import { loadRealUpstreamBaseUrlCacheStrict } from "./proxy-credentials.js";
|
|
7
8
|
export class SidecarServer {
|
|
8
9
|
server = null;
|
|
9
10
|
credentials;
|
|
@@ -58,18 +59,6 @@ export class SidecarServer {
|
|
|
58
59
|
? this.server.address().port
|
|
59
60
|
: SIDECAR_PORT;
|
|
60
61
|
}
|
|
61
|
-
/**
|
|
62
|
-
* 用持久化/合并后的真实上游 baseUrl 映射更新 credentials,避免重启后配置已是 localhost 时仍用旧缓存。
|
|
63
|
-
*/
|
|
64
|
-
updateRealUpstreamBaseUrlMap(map) {
|
|
65
|
-
for (const [id, baseUrl] of map) {
|
|
66
|
-
const creds = this.credentials.get(id);
|
|
67
|
-
if (creds && baseUrl) {
|
|
68
|
-
this.credentials.set(id, { ...creds, baseUrl });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
log(`[Sidecar] Updated real upstream baseUrl map for ${map.size} provider(s)`);
|
|
72
|
-
}
|
|
73
62
|
/** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
|
|
74
63
|
redactHeaders(h) {
|
|
75
64
|
const out = {};
|
|
@@ -95,7 +84,7 @@ export class SidecarServer {
|
|
|
95
84
|
}
|
|
96
85
|
return h;
|
|
97
86
|
}
|
|
98
|
-
/**
|
|
87
|
+
/** 仅转发到服务端 proxy(记忆注入),无本地回落。 */
|
|
99
88
|
async handleRequest(req, res) {
|
|
100
89
|
const url = req.url || "/";
|
|
101
90
|
const method = req.method?.toUpperCase();
|
|
@@ -106,12 +95,12 @@ export class SidecarServer {
|
|
|
106
95
|
return;
|
|
107
96
|
}
|
|
108
97
|
const reqId = `req-${Date.now()}`;
|
|
109
|
-
log(`[Sidecar] ${reqId} Incoming ${method} ${url}`);
|
|
110
98
|
let body;
|
|
111
99
|
try {
|
|
112
100
|
body = await this.readBody(req);
|
|
113
101
|
}
|
|
114
102
|
catch (e) {
|
|
103
|
+
log(`[Sidecar] ${reqId} err read body: ${e?.message ?? "Read body failed"}`, { console: true });
|
|
115
104
|
res.writeHead(502, { "Content-Type": "application/json" });
|
|
116
105
|
res.end(JSON.stringify({ error: e?.message || "Read body failed" }));
|
|
117
106
|
return;
|
|
@@ -123,11 +112,13 @@ export class SidecarServer {
|
|
|
123
112
|
catch {
|
|
124
113
|
openaiRequest = {};
|
|
125
114
|
}
|
|
126
|
-
const
|
|
115
|
+
const messages = openaiRequest.messages || [];
|
|
127
116
|
const modelStr = openaiRequest.model || "";
|
|
117
|
+
const stream = !!openaiRequest.stream;
|
|
118
|
+
const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
|
|
128
119
|
let provider = rawProvider || (modelStr.includes("/") ? modelStr.split("/")[0] : "") || "";
|
|
129
|
-
if (provider === "memoryx-
|
|
130
|
-
const real = this.availableProviders.find((p) => p.provider !== "memoryx-
|
|
120
|
+
if (provider === "memoryx-gateway") {
|
|
121
|
+
const real = this.availableProviders.find((p) => p.provider !== "memoryx-gateway");
|
|
131
122
|
provider = real ? real.provider : "";
|
|
132
123
|
}
|
|
133
124
|
if (!provider || !this.credentials.has(provider)) {
|
|
@@ -135,34 +126,73 @@ export class SidecarServer {
|
|
|
135
126
|
}
|
|
136
127
|
if (!provider || !this.credentials.has(provider)) {
|
|
137
128
|
for (const [id] of this.credentials) {
|
|
138
|
-
if (id !== "memoryx-
|
|
129
|
+
if (id !== "memoryx-gateway") {
|
|
139
130
|
provider = id;
|
|
140
131
|
break;
|
|
141
132
|
}
|
|
142
133
|
}
|
|
143
134
|
}
|
|
144
135
|
let creds = provider ? this.credentials.get(provider) : undefined;
|
|
145
|
-
if (!creds
|
|
136
|
+
if (!creds || !(creds.apiKey ?? "").trim()) {
|
|
146
137
|
for (const [id, c] of this.credentials) {
|
|
147
|
-
if (id !== "memoryx-
|
|
138
|
+
if (id !== "memoryx-gateway" && (c.apiKey ?? "").trim()) {
|
|
148
139
|
provider = id;
|
|
149
140
|
creds = c;
|
|
150
141
|
break;
|
|
151
142
|
}
|
|
152
143
|
}
|
|
153
144
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!baseUrl || !apiKey) {
|
|
145
|
+
let apiKey = (creds?.apiKey ?? "").trim();
|
|
146
|
+
if (!provider || !creds || !apiKey) {
|
|
157
147
|
res.writeHead(502, { "Content-Type": "application/json" });
|
|
158
|
-
res.end(JSON.stringify({ error: "No upstream" }));
|
|
148
|
+
res.end(JSON.stringify({ error: "No upstream (no apiKey for provider)" }));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (provider === "memoryx-gateway") {
|
|
152
|
+
const real = this.availableProviders.find((p) => p.provider !== "memoryx-gateway");
|
|
153
|
+
if (!real) {
|
|
154
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
155
|
+
res.end(JSON.stringify({
|
|
156
|
+
error: "memoryx-gateway is virtual; no real LLM provider in config. Add a provider (e.g. zai, anthropic) in OpenClaw models.providers.",
|
|
157
|
+
}));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
provider = real.provider;
|
|
161
|
+
const realCreds = this.credentials.get(provider);
|
|
162
|
+
if (!realCreds || !(realCreds.apiKey ?? "").trim()) {
|
|
163
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
164
|
+
res.end(JSON.stringify({
|
|
165
|
+
error: `No apiKey for real provider '${provider}'. Configure it in OpenClaw (models.providers or auth-profiles).`,
|
|
166
|
+
}));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
creds = realCreds;
|
|
170
|
+
apiKey = (creds.apiKey ?? "").trim();
|
|
171
|
+
}
|
|
172
|
+
let baseUrlMap;
|
|
173
|
+
try {
|
|
174
|
+
baseUrlMap = await loadRealUpstreamBaseUrlCacheStrict();
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
178
|
+
res.end(JSON.stringify({
|
|
179
|
+
error: "real-upstream-baseurl.json could not be read: " + (e?.message ?? String(e)),
|
|
180
|
+
}));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const baseUrl = baseUrlMap.get(provider)?.trim();
|
|
184
|
+
if (!baseUrl) {
|
|
185
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
186
|
+
res.end(JSON.stringify({
|
|
187
|
+
error: `Missing real upstream baseUrl for provider '${provider}'. Ensure real-upstream-baseurl.json is populated (e.g. from OpenClaw config).`,
|
|
188
|
+
}));
|
|
159
189
|
return;
|
|
160
190
|
}
|
|
161
191
|
const base = baseUrl.replace(/\/$/, "");
|
|
162
192
|
const pathFromOpenClaw = (url.split("?")[0] || "/").trim() || "/";
|
|
163
193
|
const targetUrl = base + (pathFromOpenClaw.startsWith("/") ? pathFromOpenClaw : "/" + pathFromOpenClaw);
|
|
164
194
|
const currentModel = openaiRequest.model || "";
|
|
165
|
-
if (!currentModel || currentModel === "auto" || currentModel.startsWith("memoryx-
|
|
195
|
+
if (!currentModel || currentModel === "auto" || currentModel.startsWith("memoryx-gateway/")) {
|
|
166
196
|
const firstId = creds?.models?.[0]?.id || creds?.models?.[0]?.name;
|
|
167
197
|
if (firstId)
|
|
168
198
|
openaiRequest.model = firstId;
|
|
@@ -176,25 +206,21 @@ export class SidecarServer {
|
|
|
176
206
|
agentId = accountInfo.agentId || "";
|
|
177
207
|
}
|
|
178
208
|
catch (_e) {
|
|
179
|
-
/* Do not block; proxy
|
|
209
|
+
/* Do not block; proxy may still accept request */
|
|
180
210
|
}
|
|
181
211
|
const headers = this.buildForwardHeaders(provider, apiKey);
|
|
182
|
-
const messages = openaiRequest.messages || [];
|
|
183
212
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
184
213
|
const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
|
|
185
214
|
const proxyRequestBody = {
|
|
186
215
|
targetUrl,
|
|
216
|
+
api_base: base,
|
|
187
217
|
headers,
|
|
188
218
|
body: openaiRequest,
|
|
189
219
|
searchQuery,
|
|
190
220
|
agent_id: agentId,
|
|
191
221
|
available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
|
|
192
222
|
};
|
|
193
|
-
|
|
194
|
-
log(`[Sidecar] ${reqId} → Proxy POST ${proxyUrl}`);
|
|
195
|
-
log(`[Sidecar] ${reqId} Proxy request headers: ${JSON.stringify(proxyReqHeaders)}`);
|
|
196
|
-
log(`[Sidecar] ${reqId} Proxy body: targetUrl=${targetUrl} model=${openaiRequest.model} stream=${!!openaiRequest.stream} searchQuery.length=${searchQuery.length} agent_id=${agentId || "(empty)"}`);
|
|
197
|
-
log(`[Sidecar] ${reqId} Forward headers (to upstream): ${JSON.stringify(this.redactHeaders(headers))}`);
|
|
223
|
+
log(`[Sidecar] ${reqId} ${method} ${(url.split("?")[0] || "/").trim()} provider=${provider} model=${openaiRequest.model ?? "-"} stream=${stream} → proxy target=${targetUrl}`);
|
|
198
224
|
let proxyResponse = null;
|
|
199
225
|
try {
|
|
200
226
|
proxyResponse = await fetch(proxyUrl, {
|
|
@@ -205,16 +231,28 @@ export class SidecarServer {
|
|
|
205
231
|
}
|
|
206
232
|
catch (e) {
|
|
207
233
|
proxyResponse = null;
|
|
208
|
-
log(`[Sidecar] ${reqId}
|
|
209
|
-
}
|
|
210
|
-
if (proxyResponse) {
|
|
211
|
-
log(`[Sidecar] ${reqId} Proxy response status=${proxyResponse.status} ${proxyResponse.statusText}`);
|
|
234
|
+
log(`[Sidecar] ${reqId} err proxy fetch: ${e?.message ?? String(e)}`, { console: true });
|
|
212
235
|
}
|
|
213
236
|
const proxyFailed = !proxyResponse || proxyResponse.status >= 500;
|
|
214
237
|
if (proxyFailed) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
238
|
+
if (proxyResponse?.body) {
|
|
239
|
+
try {
|
|
240
|
+
if (typeof proxyResponse.body.cancel === "function") {
|
|
241
|
+
await proxyResponse.body.cancel();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
await proxyResponse.text();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (_) {
|
|
248
|
+
/* drain failed, ignore */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const status = proxyResponse?.status ?? 502;
|
|
252
|
+
const detail = proxyResponse?.statusText || "MemoryX proxy unavailable";
|
|
253
|
+
log(`[Sidecar] ${reqId} ← ${status} proxy failed: ${detail}`, { console: true });
|
|
254
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
255
|
+
res.end(JSON.stringify({ error: detail }));
|
|
218
256
|
return;
|
|
219
257
|
}
|
|
220
258
|
const proxy = proxyResponse;
|
|
@@ -229,11 +267,9 @@ export class SidecarServer {
|
|
|
229
267
|
}
|
|
230
268
|
catch (firstErr) {
|
|
231
269
|
reader.releaseLock();
|
|
232
|
-
log(`[Sidecar] ${reqId}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
log(`[Sidecar] ${reqId} Direct response status=${directResponse.status}`);
|
|
236
|
-
this.writeResponse(res, directResponse);
|
|
270
|
+
log(`[Sidecar] ${reqId} err stream first chunk: ${firstErr?.message ?? "timeout"}`, { console: true });
|
|
271
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
272
|
+
res.end(JSON.stringify({ error: "MemoryX proxy stream failed" }));
|
|
237
273
|
return;
|
|
238
274
|
}
|
|
239
275
|
res.writeHead(proxy.status, {
|
|
@@ -253,7 +289,7 @@ export class SidecarServer {
|
|
|
253
289
|
reader.releaseLock();
|
|
254
290
|
}
|
|
255
291
|
res.end();
|
|
256
|
-
log(`[Sidecar] ${reqId}
|
|
292
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} stream`);
|
|
257
293
|
return;
|
|
258
294
|
}
|
|
259
295
|
if (!proxy.body) {
|
|
@@ -261,56 +297,16 @@ export class SidecarServer {
|
|
|
261
297
|
"Content-Type": proxy.headers.get("content-type") || "application/json",
|
|
262
298
|
});
|
|
263
299
|
res.end();
|
|
300
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} (no body)`);
|
|
264
301
|
return;
|
|
265
302
|
}
|
|
266
303
|
const text = await proxy.text();
|
|
267
|
-
log(`[Sidecar] ${reqId}
|
|
304
|
+
log(`[Sidecar] ${reqId} ← ${proxy.status} body=${text.length}`);
|
|
268
305
|
res.writeHead(proxy.status, {
|
|
269
306
|
"Content-Type": proxy.headers.get("content-type") || "application/json",
|
|
270
307
|
});
|
|
271
308
|
res.end(text);
|
|
272
309
|
}
|
|
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
310
|
readBody(req) {
|
|
315
311
|
return new Promise((resolve, reject) => {
|
|
316
312
|
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.
|
|
3
|
+
"version": "2.2.63",
|
|
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-
|
|
18
|
+
"reset-gateway-usage": "node scripts/reset-memoryx-gateway-usage.mjs"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"openclaw",
|