diff --git a/README.md b/README.md index 41a40a1..a81bebf 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ Official plugin maintained by MemTensor. A minimal OpenClaw lifecycle plugin that **recalls** memories from MemOS Cloud before each run and **adds** new messages to MemOS Cloud after each run. ## Features -- **Recall**: `before_agent_start` → `/search/memory` -- **Add**: `agent_end` → `/add/message` +- **Recall**: `before_agent_start` → `/product/search` +- **Add**: `agent_end` → `/product/add` - Uses **Token** auth (`Authorization: Token `) ## Install @@ -92,7 +92,7 @@ MEMOS_API_KEY=YOUR_TOKEN - `MEMOS_API_KEY` (required; Token auth) — get it at https://memos-dashboard.openmem.net/cn/apikeys/ - `MEMOS_USER_ID` (optional; default: `openclaw-user`) - `MEMOS_CONVERSATION_ID` (optional override) -- `MEMOS_RECALL_GLOBAL` (default: `true`; when true, search does **not** pass conversation_id) +- `MEMOS_RECALL_GLOBAL` (default: `true`; when true, search does **not** pass `session_id`) - `MEMOS_MULTI_AGENT_MODE` (default: `false`; enable multi-agent data isolation) - `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX` (optional) - `MEMOS_CONVERSATION_SUFFIX_MODE` (`none` | `counter`, default: `none`) @@ -158,24 +158,24 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`: ## How it Works - **Recall** (`before_agent_start`) - - Builds a `/search/memory` request using `user_id`, `query` (= prompt + optional prefix), and optional filters. - - Default **global recall**: when `recallGlobal=true`, it does **not** pass `conversation_id`. + - Builds a `/product/search` request using `user_id`, `query` (= prompt + optional prefix), and optional filters. + - Default **global recall**: when `recallGlobal=true`, it does **not** pass `session_id`. - Optional second-pass filtering: if `recallFilterEnabled=true`, candidates are sent to your configured model and only returned `keep` items are injected. - Injects a stable MemOS recall protocol via `appendSystemContext`, while the retrieved `` block remains in `prependContext`. - **Add** (`agent_end`) - - Builds a `/add/message` request with the **last turn** by default (user + assistant). - - Sends `messages` with `user_id`, `conversation_id`, and optional `tags/info/agent_id/app_id`. + - Builds a `/product/add` request with the **last turn** by default (user + assistant). + - Sends `messages` with `user_id`, `session_id`, and optional `tags/info/agent_id/app_id`. ## Multi-Agent Support The plugin provides native support for multi-agent architectures (via the `agent_id` parameter): - **Enable Mode**: Set `"multiAgentMode": true` in config or `MEMOS_MULTI_AGENT_MODE=true` in env variables (default is `false`). - **Dynamic Context**: When enabled, it automatically captures `ctx.agentId` during OpenClaw lifecycle hooks. (Note: the default OpenClaw agent `"main"` is ignored to preserve backwards compatibility for single-agent users). -- **Data Isolation**: The `agent_id` is automatically injected into both `/search/memory` and `/add/message` requests. This ensures completely isolated memory and message histories for different agents, even under the same user or session. +- **Data Isolation**: The `agent_id` is automatically injected into both `/product/search` and `/product/add` requests. This ensures completely isolated memory and message histories for different agents, even under the same user or session. - **Static Override**: You can also force a specific agent ID by setting `"agentId": "your_agent_id"` in the plugin's `config`. ## Notes -- `conversation_id` defaults to OpenClaw `sessionKey` (unless `conversationId` is provided). **TODO**: consider binding to OpenClaw `sessionId` directly. +- `session_id` defaults to OpenClaw `sessionKey` (unless `conversationId` is provided). **TODO**: consider binding to OpenClaw `sessionId` directly. - Optional **prefix/suffix** via env or config; `conversationSuffixMode=counter` increments on `/new` (requires `hooks.internal.enabled`). ## Acknowledgements diff --git a/README_ZH.md b/README_ZH.md index b1522ca..04cc069 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -7,8 +7,8 @@ - **添加记忆**:在每轮对话结束后把消息写回 MemOS Cloud ## 功能 -- **Recall**:`before_agent_start` → `/search/memory` -- **Add**:`agent_end` → `/add/message` +- **Recall**:`before_agent_start` → `/product/search` +- **Add**:`agent_end` → `/product/add` - 使用 **Token** 认证(`Authorization: Token `) ## 安装 @@ -94,7 +94,7 @@ MEMOS_API_KEY=YOUR_TOKEN - `MEMOS_API_KEY`(必填,Token 认证)—— 获取地址:https://memos-dashboard.openmem.net/cn/apikeys/ - `MEMOS_USER_ID`(可选,默认 `openclaw-user`) - `MEMOS_CONVERSATION_ID`(可选覆盖) -- `MEMOS_RECALL_GLOBAL`(默认 `true`;为 true 时检索不传 conversation_id) +- `MEMOS_RECALL_GLOBAL`(默认 `true`;为 true 时检索不传 `session_id`) - `MEMOS_MULTI_AGENT_MODE`(默认 `false`;是否开启多 Agent 数据隔离模式) - `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX`(可选) - `MEMOS_CONVERSATION_SUFFIX_MODE`(`none` | `counter`,默认 `none`) @@ -158,17 +158,17 @@ MEMOS_API_KEY=YOUR_TOKEN ## 工作原理 ### 1) 召回(before_agent_start) -- 组装 `/search/memory` 请求 +- 组装 `/product/search` 请求 - `user_id`、`query`(= prompt + 可选前缀) - - 默认**全局召回**:`recallGlobal=true` 时不传 `conversation_id` + - 默认**全局召回**:`recallGlobal=true` 时不传 `session_id` - 可选 `filter` / `knowledgebase_ids` - (可选)若开启 `recallFilterEnabled`,会先把 `memory/preference/tool_memory` 候选发给你配置的模型做二次筛选,只保留 `keep` 的条目 - 将稳定的 MemOS 召回协议通过 `appendSystemContext` 注入,而检索到的 `` 数据块继续通过 `prependContext` 注入 ### 2) 添加(agent_end) - 默认只写**最后一轮**(user + assistant) -- 构造 `/add/message` 请求: - - `user_id`、`conversation_id` +- 构造 `/product/add` 请求: + - `user_id`、`session_id` - `messages` 列表 - 可选 `tags / info / agent_id / app_id` @@ -176,11 +176,11 @@ MEMOS_API_KEY=YOUR_TOKEN 插件内置对多Agent模式的支持(`agent_id` 参数): - **开启模式**:需要在配置中设置 `"multiAgentMode": true` 或在环境变量中设置 `MEMOS_MULTI_AGENT_MODE=true`(默认为 `false`)。 - **动态获取**:开启后,执行生命周期钩子时会自动读取上下文中的 `ctx.agentId`。(注:OpenClaw 的默认 Agent `"main"` 会被自动忽略,以保证老用户的单 Agent 数据兼容性)。 -- **数据隔离**:在调用 `/search/memory`(检索记忆)和 `/add/message`(添加记录)时会自动附带该 `agent_id`,从而保证即使是同一用户下的不同 Agent 之间,记忆和反馈数据也是完全隔离的。 +- **数据隔离**:在调用 `/product/search`(检索记忆)和 `/product/add`(添加记录)时会自动附带该 `agent_id`,从而保证即使是同一用户下的不同 Agent 之间,记忆和反馈数据也是完全隔离的。 - **静态配置**:如果需要,也可在上述插件的 `config` 中显式指定 `"agentId": "your_agent_id"` 作为固定值。 ## 说明 -- 未显式指定 `conversation_id` 时,默认使用 OpenClaw `sessionKey`。**TODO**:后续考虑直接绑定 OpenClaw `sessionId`。 +- 未显式指定 `session_id` 时,默认使用 OpenClaw `sessionKey`。**TODO**:后续考虑直接绑定 OpenClaw `sessionId`。 - 可配置前后缀;`conversationSuffixMode=counter` 时会在 `/new` 递增(需 `hooks.internal.enabled`)。 ## 致谢 diff --git a/clawdbot.plugin.json b/clawdbot.plugin.json index 0fa5e6f..79c75e9 100644 --- a/clawdbot.plugin.json +++ b/clawdbot.plugin.json @@ -23,15 +23,15 @@ }, "conversationId": { "type": "string", - "description": "Override conversation_id" + "description": "Override session_id" }, "conversationIdPrefix": { "type": "string", - "description": "conversation_id prefix" + "description": "session_id prefix" }, "conversationIdSuffix": { "type": "string", - "description": "conversation_id suffix" + "description": "session_id suffix" }, "conversationSuffixMode": { "type": "string", diff --git a/index.js b/index.js index e0302cc..619612e 100644 --- a/index.js +++ b/index.js @@ -81,7 +81,7 @@ function buildSearchPayload(cfg, prompt, ctx) { if (!cfg.recallGlobal) { const conversationId = resolveConversationId(cfg, ctx); - if (conversationId) payload.conversation_id = conversationId; + if (conversationId) payload.session_id = conversationId; } let filterObj = cfg.filter ? JSON.parse(JSON.stringify(cfg.filter)) : null; @@ -101,42 +101,41 @@ function buildSearchPayload(cfg, prompt, ctx) { if (filterObj) payload.filter = filterObj; - if (cfg.knowledgebaseIds?.length) payload.knowledgebase_ids = cfg.knowledgebaseIds; + if (cfg.knowledgebaseIds?.length) payload.readable_cube_ids = cfg.knowledgebaseIds; - payload.memory_limit_number = cfg.memoryLimitNumber; + payload.top_k = cfg.memoryLimitNumber; payload.include_preference = cfg.includePreference; - payload.preference_limit_number = cfg.preferenceLimitNumber; - payload.include_tool_memory = cfg.includeToolMemory; - payload.tool_memory_limit_number = cfg.toolMemoryLimitNumber; + payload.pref_top_k = cfg.preferenceLimitNumber; + payload.search_tool_memory = cfg.includeToolMemory; + payload.tool_mem_top_k = cfg.toolMemoryLimitNumber; payload.relativity = cfg.relativity; return payload; } function buildAddMessagePayload(cfg, messages, ctx) { - const payload = { - user_id: cfg.userId, - conversation_id: resolveConversationId(cfg, ctx), - messages, - source: MEMOS_SOURCE, - }; - - const agentId = getEffectiveAgentId(cfg, ctx); - if (agentId) payload.agent_id = agentId; - if (cfg.appId) payload.app_id = cfg.appId; - if (cfg.tags?.length) payload.tags = cfg.tags; - const info = { source: "openclaw", sessionKey: ctx?.sessionKey, agentId: ctx?.agentId, ...(cfg.info || {}), }; - if (Object.keys(info).length > 0) payload.info = info; + const agentId = getEffectiveAgentId(cfg, ctx); + if (agentId) info.agent_id = agentId; + if (cfg.appId) info.app_id = cfg.appId; + if (cfg.allowPublic !== undefined) info.allow_public = cfg.allowPublic; + + const payload = { + user_id: cfg.userId, + session_id: resolveConversationId(cfg, ctx), + messages, + source: MEMOS_SOURCE, + info, + async_mode: cfg.asyncMode === "sync" || cfg.asyncMode === false ? "sync" : "async", + }; - payload.allow_public = cfg.allowPublic; - if (cfg.allowKnowledgebaseIds?.length) payload.allow_knowledgebase_ids = cfg.allowKnowledgebaseIds; - payload.async_mode = cfg.asyncMode; + if (cfg.tags?.length) payload.custom_tags = cfg.tags; + if (cfg.allowKnowledgebaseIds?.length) payload.writable_cube_ids = cfg.allowKnowledgebaseIds; return payload; } diff --git a/lib/memos-cloud-api.js b/lib/memos-cloud-api.js index 4735991..0633987 100644 --- a/lib/memos-cloud-api.js +++ b/lib/memos-cloud-api.js @@ -60,7 +60,56 @@ function stripQuotes(value) { export function extractResultData(result) { if (!result || typeof result !== "object") return null; - return result.data ?? result.data?.data ?? result.data?.result ?? null; + const data = result.data ?? result.data?.data ?? result.data?.result ?? null; + return normalizeSearchData(data); +} + +function flattenSearchBucket(buckets, valueMapper) { + if (!Array.isArray(buckets)) return []; + const list = []; + for (const bucket of buckets) { + const memories = Array.isArray(bucket?.memories) ? bucket.memories : []; + for (const item of memories) { + const value = valueMapper(item); + if (!value) continue; + list.push({ + ...item, + ...value, + relativity: item?.metadata?.relativity ?? item?.metadata?.score ?? 1, + create_time: item?.metadata?.create_time ?? item?.metadata?.created_at ?? item?.created_at, + }); + } + } + return list; +} + +function normalizeSearchData(data) { + if (!data || typeof data !== "object") return data; + const hasLegacyShape = + Array.isArray(data.memory_detail_list) || + Array.isArray(data.preference_detail_list) || + Array.isArray(data.tool_memory_detail_list); + if (hasLegacyShape) return data; + + const hasServerShape = + Array.isArray(data.text_mem) || Array.isArray(data.pref_mem) || Array.isArray(data.tool_mem); + if (!hasServerShape) return data; + + return { + ...data, + memory_detail_list: flattenSearchBucket(data.text_mem, (item) => ({ + memory_value: item?.memory || item?.metadata?.memory || "", + memory_key: item?.memory || item?.metadata?.memory || "", + })), + preference_detail_list: flattenSearchBucket(data.pref_mem, (item) => ({ + preference: item?.memory || item?.metadata?.memory || "", + preference_type: item?.metadata?.preference_type || "", + })), + tool_memory_detail_list: flattenSearchBucket(data.tool_mem, (item) => ({ + tool_value: item?.memory || item?.metadata?.memory || "", + })), + preference_note: data.pref_note ?? data.preference_note ?? "", + }; } function pad2(value) { @@ -318,7 +367,7 @@ function sanitizeAddMessageEntry(entry) { } export async function searchMemory(cfg, payload) { - return callApi(cfg, "/search/memory", sanitizeSearchPayload(payload)); + return callApi(cfg, "/product/search", payload); } export async function addMessage(cfg, payload) { @@ -329,7 +378,7 @@ export async function addMessage(cfg, payload) { // Fail open: if sanitization throws unexpectedly, send original payload. finalPayload = payload; } - return callApi(cfg, "/add/message", finalPayload); + return callApi(cfg, "/product/add", finalPayload); } function isInboundMetaSentinelLine(line) { diff --git a/moltbot.plugin.json b/moltbot.plugin.json index 0fa5e6f..79c75e9 100644 --- a/moltbot.plugin.json +++ b/moltbot.plugin.json @@ -23,15 +23,15 @@ }, "conversationId": { "type": "string", - "description": "Override conversation_id" + "description": "Override session_id" }, "conversationIdPrefix": { "type": "string", - "description": "conversation_id prefix" + "description": "session_id prefix" }, "conversationIdSuffix": { "type": "string", - "description": "conversation_id suffix" + "description": "session_id suffix" }, "conversationSuffixMode": { "type": "string", diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 0fa5e6f..79c75e9 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -23,15 +23,15 @@ }, "conversationId": { "type": "string", - "description": "Override conversation_id" + "description": "Override session_id" }, "conversationIdPrefix": { "type": "string", - "description": "conversation_id prefix" + "description": "session_id prefix" }, "conversationIdSuffix": { "type": "string", - "description": "conversation_id suffix" + "description": "session_id suffix" }, "conversationSuffixMode": { "type": "string",