@t0ken.ai/memoryx-openclaw-plugin 2.2.60 → 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.
@@ -0,0 +1,9 @@
1
+ export declare const LOG_FILE: string;
2
+ /**
3
+ * Layered logging: all logs go to file (stream when ready, else sync append so nothing is lost).
4
+ * When console=true, also log to console (for important/guidance messages).
5
+ */
6
+ export declare function log(message: string, options?: {
7
+ console?: boolean;
8
+ }): void;
9
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1,97 @@
1
+ /**
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.
4
+ */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { PLUGIN_DIR, MAX_LOG_FILE_BYTES } from "./constants.js";
8
+ let logStream = null;
9
+ let logStreamReady = false;
10
+ let writeCount = 0;
11
+ const CHECK_ROTATE_EVERY = 500;
12
+ export const LOG_FILE = path.join(PLUGIN_DIR, "plugin.log");
13
+ const LOG_FILE_OLD = path.join(PLUGIN_DIR, "plugin.log.old");
14
+ function ensureDir() {
15
+ if (!fs.existsSync(PLUGIN_DIR)) {
16
+ fs.mkdirSync(PLUGIN_DIR, { recursive: true });
17
+ }
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
+ }
42
+ /**
43
+ * Layered logging: all logs go to file (stream when ready, else sync append so nothing is lost).
44
+ * When console=true, also log to console (for important/guidance messages).
45
+ */
46
+ export function log(message, options = {}) {
47
+ const { console: toConsole = false } = options;
48
+ const line = `[${new Date().toISOString()}] ${message}\n`;
49
+ if (toConsole) {
50
+ console.log(`[MemoryX] ${message}`);
51
+ }
52
+ const writeToFile = () => {
53
+ try {
54
+ if (!logStreamReady) {
55
+ ensureDir();
56
+ rotateIfNeeded();
57
+ logStream = fs.createWriteStream(LOG_FILE, { flags: "a" });
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 (_) { }
78
+ }
79
+ logStream?.write(line, (err) => {
80
+ if (err) {
81
+ try {
82
+ fs.appendFileSync(LOG_FILE, line);
83
+ }
84
+ catch (_) { }
85
+ }
86
+ });
87
+ }
88
+ catch (e) {
89
+ try {
90
+ ensureDir();
91
+ fs.appendFileSync(LOG_FILE, line);
92
+ }
93
+ catch (_) { }
94
+ }
95
+ };
96
+ setImmediate(writeToFile);
97
+ }
@@ -0,0 +1,48 @@
1
+ import type { PluginConfig, RecallResult } from "./types.js";
2
+ export declare function getSDK(pluginConfig?: PluginConfig): Promise<any>;
3
+ export declare class MemoryXPlugin {
4
+ private pluginConfig;
5
+ private initialized;
6
+ private timersStarted;
7
+ constructor(pluginConfig?: PluginConfig);
8
+ init(): Promise<void>;
9
+ startTimersIfNeeded(): Promise<void>;
10
+ onMessage(role: string, content: string): Promise<boolean>;
11
+ recall(query: string, limit?: number, options?: {
12
+ time_filter?: {
13
+ start?: string;
14
+ end?: string;
15
+ };
16
+ categories?: string[];
17
+ tags?: string[];
18
+ /** Expand graph from this entity (file, command, etc.); aligns with server prompt convention */
19
+ entity_name?: string;
20
+ }): Promise<RecallResult>;
21
+ endConversation(): Promise<void>;
22
+ forget(memoryId: string): Promise<boolean>;
23
+ store(content: string, metadata?: Record<string, unknown>): Promise<{
24
+ success: boolean;
25
+ task_id?: string;
26
+ }>;
27
+ list(limit?: number, offset?: number, options?: {
28
+ time_filter?: {
29
+ start?: string;
30
+ end?: string;
31
+ };
32
+ categories?: string[];
33
+ tags?: string[];
34
+ }): Promise<any[]>;
35
+ getAccountInfo(): Promise<{
36
+ apiKey: string | null;
37
+ agentId: string | null;
38
+ agentType: string | null;
39
+ initialized: boolean;
40
+ quota?: any;
41
+ }>;
42
+ getQueueStatus(): Promise<{
43
+ success: boolean;
44
+ data?: any;
45
+ error?: string;
46
+ }>;
47
+ }
48
+ //# sourceMappingURL=plugin-core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-core.d.ts","sourceRoot":"","sources":["../src/plugin-core.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO7D,wBAAsB,MAAM,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAqBtE;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAS;gBAElB,YAAY,CAAC,EAAE,YAAY;IAIjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IASpC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB1D,MAAM,CACR,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAU,EACjB,OAAO,CAAC,EAAE;QACN,WAAW,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC/C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,gGAAgG;QAChG,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,GACF,OAAO,CAAC,YAAY,CAAC;IAoClB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAWhC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa1C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAa3G,IAAI,CACN,KAAK,GAAE,MAAW,EAClB,MAAM,GAAE,MAAU,EAClB,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GACrG,OAAO,CAAC,GAAG,EAAE,CAAC;IAgBX,cAAc,IAAI,OAAO,CAAC;QAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,WAAW,EAAE,OAAO,CAAC;QACrB,KAAK,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;IAkBI,cAAc,IAAI,OAAO,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,CAAC;QACX,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CAUL"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * MemoryX SDK lazy init and MemoryXPlugin (recall, onMessage, store, etc.).
3
+ */
4
+ import * as os from "os";
5
+ import { DEFAULT_API_BASE } from "./constants.js";
6
+ import { log } from "./logger.js";
7
+ let sdkInstance = null;
8
+ let sdkInitPromise = null;
9
+ export async function getSDK(pluginConfig) {
10
+ if (sdkInstance)
11
+ return sdkInstance;
12
+ if (sdkInitPromise)
13
+ return sdkInitPromise;
14
+ sdkInitPromise = (async () => {
15
+ const { MemoryXSDK, VERSION } = await import("@t0ken.ai/memoryx-sdk");
16
+ const agentName = os.hostname();
17
+ sdkInstance = new MemoryXSDK({
18
+ preset: "conversation",
19
+ apiUrl: pluginConfig?.apiBaseUrl || DEFAULT_API_BASE,
20
+ autoRegister: true,
21
+ agentType: "openclaw",
22
+ agentName,
23
+ autoStartTimers: false,
24
+ });
25
+ await sdkInstance.init();
26
+ const { setDebug } = await import("@t0ken.ai/memoryx-sdk");
27
+ setDebug(true);
28
+ log(`SDK v${VERSION} initialized with conversation preset (30k tokens / 5min idle)`, { console: true });
29
+ return sdkInstance;
30
+ })();
31
+ return sdkInitPromise;
32
+ }
33
+ export class MemoryXPlugin {
34
+ pluginConfig;
35
+ initialized = false;
36
+ timersStarted = false;
37
+ constructor(pluginConfig) {
38
+ this.pluginConfig = pluginConfig;
39
+ }
40
+ async init() {
41
+ if (this.initialized)
42
+ return;
43
+ this.initialized = true;
44
+ log("Async init started");
45
+ try {
46
+ await getSDK(this.pluginConfig);
47
+ log("SDK ready");
48
+ }
49
+ catch (e) {
50
+ log(`Init failed: ${e}`);
51
+ }
52
+ }
53
+ async startTimersIfNeeded() {
54
+ if (this.timersStarted)
55
+ return;
56
+ await this.init();
57
+ const sdk = await getSDK(this.pluginConfig);
58
+ sdk.startTimers();
59
+ this.timersStarted = true;
60
+ log("Timers started");
61
+ }
62
+ async onMessage(role, content) {
63
+ await this.init();
64
+ if (!content || content.length < 2)
65
+ return false;
66
+ // Skip short acknowledgments (e.g. ok, thanks, hello, bye; includes common Chinese equivalents)
67
+ const skipPatterns = [
68
+ /^[好的ok谢谢嗯啊哈哈你好hihello拜拜再见]{1,5}$/i,
69
+ /^[??!!。,,\s]{1,10}$/,
70
+ ];
71
+ for (const pattern of skipPatterns) {
72
+ if (pattern.test(content.trim()))
73
+ return false;
74
+ }
75
+ try {
76
+ const sdk = await getSDK(this.pluginConfig);
77
+ await sdk.addMessage(role, content);
78
+ return true;
79
+ }
80
+ catch (e) {
81
+ log(`onMessage failed: ${e}`);
82
+ return false;
83
+ }
84
+ }
85
+ async recall(query, limit = 5, options) {
86
+ await this.init();
87
+ try {
88
+ const sdk = await getSDK(this.pluginConfig);
89
+ const result = await sdk.search?.(query, limit, options) ?? sdk.search(query, limit, options);
90
+ const isLimited = result.is_limited ??
91
+ (result.search_count !== undefined &&
92
+ result.search_limit !== undefined &&
93
+ result.search_count >= result.search_limit);
94
+ const upgradeHint = isLimited
95
+ ? `Search quota exceeded (${result.search_count}/${result.search_limit}). Upgrade to Pro for unlimited searches.`
96
+ : undefined;
97
+ return {
98
+ memories: (result.data || []).map((m) => ({
99
+ id: m.id,
100
+ content: m.content || m.memory,
101
+ category: m.category || "other",
102
+ score: m.score || 0.5,
103
+ })),
104
+ relatedMemories: (result.related_memories || []).map((m) => ({
105
+ id: m.id,
106
+ content: m.content || m.memory,
107
+ category: m.category || "other",
108
+ score: m.score || 0,
109
+ })),
110
+ isLimited,
111
+ remainingQuota: result.remaining_quota ?? -1,
112
+ upgradeHint,
113
+ };
114
+ }
115
+ catch (e) {
116
+ log(`Recall failed: ${e}`);
117
+ return { memories: [], relatedMemories: [], isLimited: false, remainingQuota: 0 };
118
+ }
119
+ }
120
+ async endConversation() {
121
+ try {
122
+ const sdk = await getSDK(this.pluginConfig);
123
+ await sdk.flush();
124
+ log("Conversation ended, flushed queue");
125
+ sdk.startNewConversation();
126
+ }
127
+ catch (e) {
128
+ log(`End conversation failed: ${e}`);
129
+ }
130
+ }
131
+ async forget(memoryId) {
132
+ await this.init();
133
+ try {
134
+ const sdk = await getSDK(this.pluginConfig);
135
+ await sdk.delete(memoryId);
136
+ log(`Forgot memory ${memoryId}`);
137
+ return true;
138
+ }
139
+ catch (e) {
140
+ log(`Forget failed: ${e}`);
141
+ return false;
142
+ }
143
+ }
144
+ async store(content, metadata) {
145
+ await this.init();
146
+ try {
147
+ const sdk = await getSDK(this.pluginConfig);
148
+ const result = await sdk.addMemory?.(content, metadata ?? {}) ?? sdk.addMemory(content);
149
+ log(`Stored memory, result: ${JSON.stringify(result)}`);
150
+ return { success: true, task_id: result?.task_id };
151
+ }
152
+ catch (e) {
153
+ log(`Store failed: ${e}`);
154
+ return { success: false };
155
+ }
156
+ }
157
+ async list(limit = 10, offset = 0, options) {
158
+ await this.init();
159
+ try {
160
+ const sdk = await getSDK(this.pluginConfig);
161
+ const result = await sdk.list?.(limit, offset, options) ?? sdk.list(limit, offset);
162
+ return (result.data || result.memories || []).map((m) => ({
163
+ id: m.id,
164
+ content: m.content || m.memory,
165
+ category: m.category || "other",
166
+ }));
167
+ }
168
+ catch (e) {
169
+ log(`List failed: ${e}`);
170
+ return [];
171
+ }
172
+ }
173
+ async getAccountInfo() {
174
+ await this.init();
175
+ try {
176
+ const sdk = await getSDK(this.pluginConfig);
177
+ const info = await sdk.getAccountInfo();
178
+ const agentType = sdk.getAgentType();
179
+ return {
180
+ apiKey: info.apiKey,
181
+ agentId: info.agentId,
182
+ agentType: agentType,
183
+ initialized: info.initialized,
184
+ quota: info.quota,
185
+ };
186
+ }
187
+ catch (e) {
188
+ return { apiKey: null, agentId: null, agentType: null, initialized: false };
189
+ }
190
+ }
191
+ async getQueueStatus() {
192
+ await this.init();
193
+ try {
194
+ const sdk = await getSDK(this.pluginConfig);
195
+ return await sdk.getQueueStatus();
196
+ }
197
+ catch (e) {
198
+ log(`GetQueueStatus failed: ${e}`);
199
+ return { success: false, error: String(e) };
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Extract and resolve provider credentials from OpenClaw config; default model/provider and available list for fallback.
3
+ *
4
+ * API key sources:
5
+ * - OpenClaw may store keys in auth-profiles.json (e.g. openclaw models auth paste-token);
6
+ * the gateway does auth lookup at runtime and does not merge back into api.config.
7
+ * - The plugin only receives api.config (from openclaw.json), so it can read:
8
+ * config.models.providers[id].apiKey, env vars, and auth-profiles.json (this module).
9
+ * - If the user configured key only via auth-profiles and not in config/env, we try to
10
+ * fill apiKey from ~/.openclaw/agents/main/agent/auth-profiles.json by provider.
11
+ */
12
+ import type { ProviderCredentials } from "./types.js";
13
+ export declare function extractProviderCredentials(config: any, providerOverrides?: Record<string, {
14
+ baseUrl?: string;
15
+ apiKeyEnv?: string;
16
+ apiKey?: string;
17
+ }>): Map<string, ProviderCredentials>;
18
+ /**
19
+ * 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
20
+ * - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
21
+ * - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
22
+ * 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
23
+ */
24
+ export declare function buildRealUpstreamBaseUrlMap(credentials: Map<string, ProviderCredentials>, _sidecarBaseUrl: string): Map<string, string>;
25
+ /**
26
+ * 用 init 时缓存的「真实上游 baseUrl 映射表」生成供 Sidecar 使用的 credentials。
27
+ * 发给 llm_proxy 的每个厂商的 targetUrl 均来自此映射表(初始化时的真实 baseUrl),不会被 redirect 后的 localhost 覆盖。
28
+ * 用户改配置后重启即可生效。
29
+ */
30
+ export declare function realUpstreamCredentialsForSidecar(credentials: Map<string, ProviderCredentials>, realUpstreamBaseUrlMap: Map<string, string>): Map<string, ProviderCredentials>;
31
+ /**
32
+ * 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
33
+ */
34
+ export declare function loadRealUpstreamBaseUrlCache(): Promise<Map<string, string>>;
35
+ /**
36
+ * 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
37
+ */
38
+ export declare function saveRealUpstreamBaseUrlCache(map: Map<string, string>): Promise<void>;
39
+ /** 判断是否为 localhost(或等于 sidecarBaseUrl),用于合并缓存时决定是否用缓存值覆盖。 */
40
+ export declare function isLocalhostBaseUrl(url: string, sidecarBaseUrl: string): boolean;
41
+ /** Get all available provider/model list (for server fallback/retry); only providers with apiKey set */
42
+ export declare function getAvailableProviders(credentials: Map<string, ProviderCredentials>): Array<{
43
+ provider: string;
44
+ model: string;
45
+ }>;
46
+ /**
47
+ * Default primary: one model per provider (first listed); only providers with apiKey set.
48
+ * Order follows config.models.providers key order (Map insertion order).
49
+ */
50
+ export declare function getDefaultProviderModelList(credentials: Map<string, ProviderCredentials>): Array<{
51
+ provider: string;
52
+ model: string;
53
+ }>;
54
+ /** Get default model/provider and full availableProviders; prefer config.agents.defaults.model.primary, else first in list. */
55
+ export declare function getDefaultModelAndProvider(credentials: Map<string, ProviderCredentials>, config?: any): {
56
+ model: string;
57
+ provider: string;
58
+ availableProviders: Array<{
59
+ provider: string;
60
+ model: string;
61
+ }>;
62
+ };
63
+ //# sourceMappingURL=proxy-credentials.d.ts.map
@@ -0,0 +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,CA0ClC;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;;GAEG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcjF;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"}
@@ -0,0 +1,234 @@
1
+ import { log } from "./logger.js";
2
+ import { PLUGIN_DIR, REAL_UPSTREAM_BASEURL_CACHE_FILE } from "./constants.js";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import * as os from "os";
6
+ const DEFAULT_AUTH_PROFILES_PATH = path.join(os.homedir(), ".openclaw", "agents", "main", "agent", "auth-profiles.json");
7
+ /** Parse apiKey by provider from auth-profiles.json. Profile keys are often "providerId" or "providerId:name", matching models.providers id. */
8
+ function loadAuthProfilesKeys(profilesPath) {
9
+ const out = new Map();
10
+ try {
11
+ if (!fs.existsSync(profilesPath))
12
+ return out;
13
+ const raw = fs.readFileSync(profilesPath, "utf8");
14
+ const data = JSON.parse(raw);
15
+ const profiles = data?.profiles ?? {};
16
+ for (const [profileId, p] of Object.entries(profiles)) {
17
+ if (!p || typeof p !== "object")
18
+ continue;
19
+ const isApiKey = (p.type === "api_key" || p.mode === "api_key");
20
+ const value = (p.key ?? p.token ?? "")?.trim();
21
+ if (!isApiKey || !value)
22
+ continue;
23
+ const providerId = profileId.includes(":") ? profileId.split(":")[0] : profileId;
24
+ if (!out.has(providerId))
25
+ out.set(providerId, value);
26
+ if (p.provider && p.provider !== providerId && !out.has(p.provider))
27
+ out.set(p.provider, value);
28
+ }
29
+ }
30
+ catch (_) {
31
+ // ignore
32
+ }
33
+ return out;
34
+ }
35
+ export function extractProviderCredentials(config, providerOverrides) {
36
+ const credentials = new Map();
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
+ if (config?.models?.providers) {
42
+ for (const [id, providerConfig] of Object.entries(config.models.providers)) {
43
+ if (typeof providerConfig !== "object" ||
44
+ providerConfig === null ||
45
+ !("baseUrl" in providerConfig)) {
46
+ continue;
47
+ }
48
+ const pc = providerConfig;
49
+ if (pc.baseUrl) {
50
+ const override = providerOverrides?.[id];
51
+ const customEnvName = override?.apiKeyEnv;
52
+ const customApiKey = override?.apiKey;
53
+ const apiKey = customApiKey ||
54
+ (typeof pc.apiKey === "string" ? pc.apiKey.trim() : "") ||
55
+ (customEnvName ? process.env[customEnvName] : undefined) ||
56
+ process.env[`${id.toUpperCase()}_API_KEY`] ||
57
+ authKeys.get(id) ||
58
+ "";
59
+ const isMemoryxGateway = (id === "memoryx-gateway");
60
+ credentials.set(id, {
61
+ baseUrl: isMemoryxGateway ? "" : (override?.baseUrl || pc.baseUrl),
62
+ apiKey,
63
+ models: pc.models,
64
+ });
65
+ log(`[Proxy] Extracted credentials for ${id}: models=${pc.models?.length || 0}`);
66
+ }
67
+ }
68
+ }
69
+ return credentials;
70
+ }
71
+ /**
72
+ * 在 init 时从 OpenClaw 配置(api.config)读到的「所有厂商 -> baseUrl」做一次快照,不写死任何厂商地址。
73
+ * - 来源唯一:插件启动时通过 api 读到的 openclaw 配置文件(openclaw.json 等)里的 models.providers[id].baseUrl。
74
+ * - 若某条已是 localhost(例如上次 redirect 被持久化),本次仍写入 localhost;恢复依赖 setImmediate 里从缓存文件合并(首次正常时已落盘)。
75
+ * 返回 Map<providerId, baseUrl>,memoryx-gateway 不写入。
76
+ */
77
+ export function buildRealUpstreamBaseUrlMap(credentials, _sidecarBaseUrl) {
78
+ const map = new Map();
79
+ for (const [id, creds] of credentials) {
80
+ if (id === "memoryx-gateway")
81
+ continue;
82
+ const base = (creds.baseUrl ?? "").trim();
83
+ map.set(id, base);
84
+ }
85
+ return map;
86
+ }
87
+ /**
88
+ * 用 init 时缓存的「真实上游 baseUrl 映射表」生成供 Sidecar 使用的 credentials。
89
+ * 发给 llm_proxy 的每个厂商的 targetUrl 均来自此映射表(初始化时的真实 baseUrl),不会被 redirect 后的 localhost 覆盖。
90
+ * 用户改配置后重启即可生效。
91
+ */
92
+ export function realUpstreamCredentialsForSidecar(credentials, realUpstreamBaseUrlMap) {
93
+ const out = new Map();
94
+ for (const [id, creds] of credentials) {
95
+ if (id === "memoryx-gateway") {
96
+ out.set(id, creds);
97
+ continue;
98
+ }
99
+ const baseUrl = realUpstreamBaseUrlMap.get(id) ?? creds.baseUrl ?? "";
100
+ out.set(id, { ...creds, baseUrl });
101
+ }
102
+ return out;
103
+ }
104
+ /**
105
+ * 从缓存文件加载「厂商 -> 真实 baseUrl」映射。文件不存在或解析失败返回空 Map。
106
+ */
107
+ export async function loadRealUpstreamBaseUrlCache() {
108
+ try {
109
+ const raw = await fs.promises.readFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, "utf8");
110
+ const obj = JSON.parse(raw);
111
+ const map = new Map();
112
+ if (obj && typeof obj === "object") {
113
+ for (const [id, url] of Object.entries(obj)) {
114
+ if (id && typeof url === "string" && url.trim())
115
+ map.set(id, url.trim());
116
+ }
117
+ }
118
+ return map;
119
+ }
120
+ catch {
121
+ return new Map();
122
+ }
123
+ }
124
+ /**
125
+ * 将「厂商 -> 真实 baseUrl」映射写入缓存文件,重启后若配置被改成 localhost 可从此恢复。
126
+ */
127
+ export async function saveRealUpstreamBaseUrlCache(map) {
128
+ try {
129
+ await fs.promises.mkdir(PLUGIN_DIR, { recursive: true });
130
+ const obj = {};
131
+ for (const [id, url] of map) {
132
+ if (id !== "memoryx-gateway" && url)
133
+ obj[id] = url;
134
+ }
135
+ await fs.promises.writeFile(REAL_UPSTREAM_BASEURL_CACHE_FILE, JSON.stringify(obj, null, 2), "utf8");
136
+ log(`[Proxy] Saved real upstream baseUrl cache to ${REAL_UPSTREAM_BASEURL_CACHE_FILE}`);
137
+ }
138
+ catch (e) {
139
+ log(`[Proxy] Failed to save real upstream cache: ${e?.message ?? e}`);
140
+ }
141
+ }
142
+ /** 判断是否为 localhost(或等于 sidecarBaseUrl),用于合并缓存时决定是否用缓存值覆盖。 */
143
+ export function isLocalhostBaseUrl(url, sidecarBaseUrl) {
144
+ const u = (url || "").trim().toLowerCase().replace(/\/$/, "");
145
+ const sidecarNorm = (sidecarBaseUrl || "").toLowerCase().replace(/\/$/, "");
146
+ return !u || u.includes("localhost") || (!!sidecarNorm && u === sidecarNorm);
147
+ }
148
+ /** Only include providers with non-empty apiKey to avoid invalid Bearer header */
149
+ function hasApiKey(creds) {
150
+ return ((creds.apiKey ?? "").trim().length > 0);
151
+ }
152
+ /** Get all available provider/model list (for server fallback/retry); only providers with apiKey set */
153
+ export function getAvailableProviders(credentials) {
154
+ const result = [];
155
+ for (const [providerId, creds] of credentials) {
156
+ if (!hasApiKey(creds))
157
+ continue;
158
+ if (creds.models && creds.models.length > 0) {
159
+ for (const m of creds.models) {
160
+ const id = m.id || m.name;
161
+ if (id)
162
+ result.push({ provider: providerId, model: id });
163
+ }
164
+ }
165
+ else {
166
+ result.push({ provider: providerId, model: "" });
167
+ }
168
+ }
169
+ return result;
170
+ }
171
+ /**
172
+ * Default primary: one model per provider (first listed); only providers with apiKey set.
173
+ * Order follows config.models.providers key order (Map insertion order).
174
+ */
175
+ export function getDefaultProviderModelList(credentials) {
176
+ const result = [];
177
+ for (const [providerId, creds] of credentials) {
178
+ if (!hasApiKey(creds))
179
+ continue;
180
+ if (creds.models?.length) {
181
+ const id = creds.models[0].id || creds.models[0].name;
182
+ if (id)
183
+ result.push({ provider: providerId, model: id });
184
+ }
185
+ }
186
+ return result;
187
+ }
188
+ /** Find provider/model in available list matching primary (primary format: "provider/modelId") */
189
+ function resolvePrimaryInAvailable(primary, available) {
190
+ if (!primary || typeof primary !== "string")
191
+ return null;
192
+ const trimmed = primary.trim();
193
+ const slash = trimmed.indexOf("/");
194
+ const wantProvider = slash > 0 ? trimmed.slice(0, slash) : trimmed;
195
+ const wantModel = slash > 0 ? trimmed.slice(slash + 1) : "";
196
+ for (const a of available) {
197
+ if (a.provider !== wantProvider)
198
+ continue;
199
+ if (!wantModel || a.model === wantModel || a.model.endsWith("/" + wantModel))
200
+ return a;
201
+ }
202
+ if (wantProvider && wantModel)
203
+ return { provider: wantProvider, model: wantModel };
204
+ return null;
205
+ }
206
+ /** Get default model/provider and full availableProviders; prefer config.agents.defaults.model.primary, else first in list. */
207
+ export function getDefaultModelAndProvider(credentials, config) {
208
+ const defaultList = getDefaultProviderModelList(credentials);
209
+ const availableAll = getAvailableProviders(credentials);
210
+ const primaryFromConfig = config?.agents?.defaults?.model?.primary ?? config?.agents?.defaults?.model?.default;
211
+ const resolved = resolvePrimaryInAvailable(typeof primaryFromConfig === "string" ? primaryFromConfig : "", availableAll.length > 0 ? availableAll : defaultList);
212
+ if (resolved) {
213
+ return { model: resolved.model, provider: resolved.provider, availableProviders: availableAll.length > 0 ? availableAll : defaultList };
214
+ }
215
+ if (defaultList.length > 0) {
216
+ return {
217
+ model: defaultList[0].model,
218
+ provider: defaultList[0].provider,
219
+ availableProviders: availableAll,
220
+ };
221
+ }
222
+ const agentsDefaults = config?.agents?.defaults;
223
+ let model = "claude-sonnet-4-20250514";
224
+ let provider = "anthropic";
225
+ if (agentsDefaults?.model) {
226
+ const m = agentsDefaults.model;
227
+ model = typeof m === "string" ? m : (m.default ? String(m.default) : model);
228
+ }
229
+ if (agentsDefaults?.provider) {
230
+ const p = agentsDefaults.provider;
231
+ provider = typeof p === "string" ? p : (p.default ? String(p.default) : provider);
232
+ }
233
+ return { model, provider, availableProviders: [{ provider, model }] };
234
+ }
@@ -0,0 +1,7 @@
1
+ export declare function createProxyRedirect(api: any, getSidecarBase: () => string, realProviderHeader: string): {
2
+ applySidecarRedirect: (opts?: {
3
+ quiet?: boolean;
4
+ }) => void;
5
+ wrapProvidersWithProxy: () => void;
6
+ };
7
+ //# sourceMappingURL=proxy-redirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy-redirect.d.ts","sourceRoot":"","sources":["../src/proxy-redirect.ts"],"names":[],"mappings":"AAMA,wBAAgB,mBAAmB,CAC/B,GAAG,EAAE,GAAG,EACR,cAAc,EAAE,MAAM,MAAM,EAC5B,kBAAkB,EAAE,MAAM,GAC3B;IACC,oBAAoB,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3D,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACtC,CAoEA"}