@memtensor/memos-local-openclaw-plugin 1.0.4-beta.2 → 1.0.4-beta.20
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/.env.example +7 -0
- package/README.md +111 -44
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +36 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +6 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +160 -26
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -3
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +9 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +500 -112
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +11 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +31 -3
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +84 -9
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +4 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +59 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +115 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +458 -110
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2952 -910
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +39 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1198 -227
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +774 -74
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +40 -1
- package/src/client/connector.ts +161 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +2 -3
- package/src/hub/server.ts +481 -107
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +99 -10
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +63 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +498 -137
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +12 -2
- package/src/viewer/html.ts +2952 -910
- package/src/viewer/server.ts +1109 -212
package/openclaw.plugin.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "memos-local-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Local Memory",
|
|
4
|
-
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency). Provides memory_search, memory_get, task_summary,
|
|
4
|
+
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
|
|
5
5
|
"kind": "memory",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.12",
|
|
7
7
|
"skills": [
|
|
8
8
|
"skill/memos-memory-guide"
|
|
9
9
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "1.0.4-beta.
|
|
3
|
+
"version": "1.0.4-beta.20",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
"src",
|
|
11
11
|
"dist",
|
|
12
12
|
"skill",
|
|
13
|
+
"prebuilds",
|
|
13
14
|
"scripts/postinstall.cjs",
|
|
14
15
|
"openclaw.plugin.json",
|
|
16
|
+
"telemetry.credentials.json",
|
|
15
17
|
"README.md",
|
|
16
18
|
".env.example"
|
|
17
19
|
],
|
|
@@ -50,7 +52,6 @@
|
|
|
50
52
|
"@huggingface/transformers": "^3.8.0",
|
|
51
53
|
"@sinclair/typebox": "^0.34.48",
|
|
52
54
|
"better-sqlite3": "^12.6.2",
|
|
53
|
-
"posthog-node": "^5.28.0",
|
|
54
55
|
"puppeteer": "^24.38.0",
|
|
55
56
|
"semver": "^7.7.4",
|
|
56
57
|
"uuid": "^10.0.0"
|
package/scripts/postinstall.cjs
CHANGED
|
@@ -112,7 +112,7 @@ try {
|
|
|
112
112
|
function ensureDependencies() {
|
|
113
113
|
phase(0, "检测核心依赖 / Check core dependencies");
|
|
114
114
|
|
|
115
|
-
const coreDeps = ["@sinclair/typebox", "uuid", "
|
|
115
|
+
const coreDeps = ["@sinclair/typebox", "uuid", "@huggingface/transformers"];
|
|
116
116
|
const missing = [];
|
|
117
117
|
for (const dep of coreDeps) {
|
|
118
118
|
try {
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memos-memory-guide
|
|
3
|
-
description: "Use the MemOS Local memory system to search and use the user's past conversations. Use this skill whenever the user refers to past chats, their own preferences or history, or when you need to answer from prior context. When auto-recall returns nothing (long or unclear user query), generate your own short search query and call memory_search. Available tools: memory_search, memory_get, memory_write_public, task_summary, skill_get, skill_search, skill_install, skill_publish, skill_unpublish, memory_timeline, memory_viewer."
|
|
3
|
+
description: "Use the MemOS Local memory system to search and use the user's past conversations. Use this skill whenever the user refers to past chats, their own preferences or history, or when you need to answer from prior context. When auto-recall returns nothing (long or unclear user query), generate your own short search query and call memory_search. Available tools: memory_search, memory_get, memory_write_public, memory_share, memory_unshare, task_summary, skill_get, skill_search, skill_install, skill_publish, skill_unpublish, network_memory_detail, network_skill_pull, network_team_info, memory_timeline, memory_viewer."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# MemOS Local Memory — Agent Guide
|
|
7
7
|
|
|
8
|
-
This skill describes how to use the MemOS memory tools so you can reliably search and use the user's long-term conversation history, query
|
|
8
|
+
This skill describes how to use the MemOS memory tools so you can reliably search and use the user's long-term conversation history, query team-shared data, share tasks, and discover or pull reusable skills.
|
|
9
|
+
|
|
10
|
+
Two sharing planes exist and must not be confused:
|
|
11
|
+
|
|
12
|
+
- **Local agent sharing:** visible to agents in the same OpenClaw workspace only.
|
|
13
|
+
- **Team sharing:** visible to teammates through the configured team server.
|
|
9
14
|
|
|
10
15
|
## How memory is provided each turn
|
|
11
16
|
|
|
12
17
|
- **Automatic recall (hook):** At the start of each turn, the system runs a memory search using the user's current message and injects relevant past memories into your context. You do not need to call any tool for that.
|
|
13
18
|
- **When that is not enough:** If the user's message is very long, vague, or the automatic search returns **no memories**, you should **generate your own short, focused query** and call `memory_search` yourself.
|
|
14
|
-
- **Memory isolation:** Each agent can only see its own local private memories and local `public` memories.
|
|
19
|
+
- **Memory isolation:** Each agent can only see its own local private memories and local `public` memories. Team-shared data only appears when you search with `scope="group"` or `scope="all"`.
|
|
15
20
|
|
|
16
21
|
## Tools — what they do and when to call
|
|
17
22
|
|
|
@@ -24,9 +29,10 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
|
|
|
24
29
|
- You need to search with a different angle (e.g. filter by `role='user'`).
|
|
25
30
|
- **Parameters:**
|
|
26
31
|
- `query` (string, **required**) — Natural language search query.
|
|
27
|
-
- `
|
|
28
|
-
- `
|
|
29
|
-
- `
|
|
32
|
+
- `scope` (string, optional) — `'local'` (default) for current agent + local shared memories, or `'group'` / `'all'` to include team-shared memories.
|
|
33
|
+
- `maxResults` (number, optional) — Increase when the first search is too narrow.
|
|
34
|
+
- `minScore` (number, optional) — Lower slightly if recall is too strict.
|
|
35
|
+
- `role` (string, optional) — Filter local results by `'user'`, `'assistant'`, `'tool'`, or `'system'`.
|
|
30
36
|
|
|
31
37
|
### memory_get
|
|
32
38
|
|
|
@@ -38,12 +44,32 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
|
|
|
38
44
|
|
|
39
45
|
### memory_write_public
|
|
40
46
|
|
|
41
|
-
- **What it does:**
|
|
42
|
-
- **When to call:** In multi-agent or collaborative scenarios, when you
|
|
47
|
+
- **What it does:** Create a brand new local shared memory. These memories are visible to all agents in the same OpenClaw workspace during `memory_search`. This does **not** publish anything to the team server.
|
|
48
|
+
- **When to call:** In multi-agent or collaborative scenarios, when you want to create a new persistent shared note from scratch (e.g. shared decisions, conventions, configurations, workflows). Do not use it if you already have a specific memory chunk to expose.
|
|
43
49
|
- **Parameters:**
|
|
44
|
-
- `content` (string, **required**) — The content to write to
|
|
50
|
+
- `content` (string, **required**) — The content to write to local shared memory.
|
|
45
51
|
- `summary` (string, optional) — Short summary of the content.
|
|
46
52
|
|
|
53
|
+
### memory_share
|
|
54
|
+
|
|
55
|
+
- **What it does:** Share an existing memory either with local OpenClaw agents, to the team, or to both.
|
|
56
|
+
- **When to call:** You already have a useful memory chunk and want to expose it beyond the current agent.
|
|
57
|
+
- **Do not use when:** You are creating a new shared note from scratch. In that case use `memory_write_public`.
|
|
58
|
+
- **Parameters:**
|
|
59
|
+
- `chunkId` (string, **required**) — Existing memory chunk ID.
|
|
60
|
+
- `target` (string, optional) — `'agents'` (default), `'hub'`, or `'both'`.
|
|
61
|
+
- `visibility` (string, optional) — Team visibility when target includes team: `'public'` (default) or `'group'`.
|
|
62
|
+
- `groupId` (string, optional) — Optional team group ID when `visibility='group'`.
|
|
63
|
+
|
|
64
|
+
### memory_unshare
|
|
65
|
+
|
|
66
|
+
- **What it does:** Remove an existing memory from local agent sharing, team sharing, or both.
|
|
67
|
+
- **When to call:** A memory should no longer be visible outside the current agent or should be removed from the team.
|
|
68
|
+
- **Parameters:**
|
|
69
|
+
- `chunkId` (string, **required**) — Existing memory chunk ID.
|
|
70
|
+
- `target` (string, optional) — `'agents'`, `'hub'`, or `'all'` (default).
|
|
71
|
+
- `privateOwner` (string, optional) — Rare fallback only for older public memories that have no recorded original owner.
|
|
72
|
+
|
|
47
73
|
### task_summary
|
|
48
74
|
|
|
49
75
|
- **What it does:** Get the detailed summary of a complete task: title, status, narrative summary, and related skills. Use when `memory_search` returns a hit with a `task_id` and you need the full story. Preserves critical information: URLs, file paths, commands, error codes, step-by-step instructions.
|
|
@@ -62,11 +88,11 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
|
|
|
62
88
|
|
|
63
89
|
### skill_search
|
|
64
90
|
|
|
65
|
-
- **What it does:** Search available skills by natural language. Searches your own skills,
|
|
91
|
+
- **What it does:** Search available skills by natural language. Searches your own skills, local shared skills, or both. It can also include team skills.
|
|
66
92
|
- **When to call:** The current task requires a capability or guide you don't have. Use `skill_search` to find one first; after finding it, use `skill_get` to read it, then `skill_install` to load it for future turns.
|
|
67
93
|
- **Parameters:**
|
|
68
94
|
- `query` (string, **required**) — Natural language description of the needed skill.
|
|
69
|
-
- `scope` (string, optional) —
|
|
95
|
+
- `scope` (string, optional) — `'mix'` (default, self + local shared), `'self'`, `'public'` (local shared only), or `'group'` / `'all'` to include team results.
|
|
70
96
|
|
|
71
97
|
### skill_install
|
|
72
98
|
|
|
@@ -77,40 +103,46 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
|
|
|
77
103
|
|
|
78
104
|
### skill_publish
|
|
79
105
|
|
|
80
|
-
- **What it does:**
|
|
81
|
-
- **When to call:** You have a useful skill that other agents could benefit from
|
|
106
|
+
- **What it does:** Share a skill with local agents, or publish it to the team.
|
|
107
|
+
- **When to call:** You have a useful skill that other agents or your team could benefit from.
|
|
82
108
|
- **Parameters:**
|
|
83
109
|
- `skillId` (string, **required**) — The skill ID to publish.
|
|
110
|
+
- `target` (string, optional) — `'agents'` (default) or `'hub'`.
|
|
111
|
+
- `visibility` (string, optional) — When `target='hub'`, use `'public'` (default) or `'group'`.
|
|
112
|
+
- `groupId` (string, optional) — Optional team group ID when `target='hub'` and `visibility='group'`.
|
|
113
|
+
- `scope` (string, optional) — Backward-compatible alias for old calls. Prefer `target` + `visibility` in new calls.
|
|
84
114
|
|
|
85
115
|
### skill_unpublish
|
|
86
116
|
|
|
87
|
-
- **What it does:**
|
|
117
|
+
- **What it does:** Stop local agent sharing, remove a team-published copy, or do both.
|
|
88
118
|
- **When to call:** You want to stop sharing a previously published skill.
|
|
89
119
|
- **Parameters:**
|
|
90
120
|
- `skillId` (string, **required**) — The skill ID to unpublish.
|
|
121
|
+
- `target` (string, optional) — `'agents'` (default), `'hub'`, or `'all'`.
|
|
91
122
|
|
|
92
123
|
### network_memory_detail
|
|
93
124
|
|
|
94
|
-
- **What it does:** Fetches the full content behind a
|
|
95
|
-
- **When to call:** A `memory_search` result came from the
|
|
125
|
+
- **What it does:** Fetches the full content behind a team search hit.
|
|
126
|
+
- **When to call:** A `memory_search` result came from the team and you need the full shared memory content.
|
|
96
127
|
- **Parameters:** `remoteHitId`.
|
|
97
128
|
|
|
98
129
|
### task_share / task_unshare
|
|
99
130
|
|
|
100
|
-
- **What they do:** Share a local task to the
|
|
131
|
+
- **What they do:** Share a local task to the team, or remove it later.
|
|
101
132
|
- **When to call:** A task is valuable to your group or to the whole team and should be discoverable via shared search.
|
|
102
133
|
- **Parameters:** `taskId`, plus sharing visibility/scope when required.
|
|
103
134
|
|
|
104
135
|
### network_skill_pull
|
|
105
136
|
|
|
106
|
-
- **What it does:** Pulls a
|
|
107
|
-
- **When to call:** `skill_search` found a useful
|
|
137
|
+
- **What it does:** Pulls a team-shared skill bundle down into local storage.
|
|
138
|
+
- **When to call:** `skill_search` found a useful team skill and you want to use it locally or offline.
|
|
108
139
|
- **Parameters:** `skillId`.
|
|
109
140
|
|
|
110
141
|
### network_team_info
|
|
111
142
|
|
|
112
|
-
- **What it does:** Returns current
|
|
143
|
+
- **What it does:** Returns current team server connection information, user, role, and groups.
|
|
113
144
|
- **When to call:** You need to confirm whether team sharing is configured or which groups the current client belongs to.
|
|
145
|
+
- **Call this first before:** `memory_share(... target='hub'|'both')`, `memory_unshare(... target='hub'|'all')`, `task_share`, `task_unshare`, `skill_publish(... target='hub')`, `skill_unpublish(... target='hub'|'all')`, or `network_skill_pull`.
|
|
114
146
|
- **Parameters:** none.
|
|
115
147
|
|
|
116
148
|
### memory_timeline
|
|
@@ -147,13 +179,19 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
|
|
|
147
179
|
6. **You need a capability/guide that you don't have**
|
|
148
180
|
→ Call `skill_search(query="...", scope="mix")` to discover available skills.
|
|
149
181
|
|
|
150
|
-
7. **You have shared knowledge useful to all agents**
|
|
151
|
-
→ Call `memory_write_public(content="...")
|
|
182
|
+
7. **You have new shared knowledge useful to all local agents**
|
|
183
|
+
→ Call `memory_write_public(content="...")`.
|
|
184
|
+
|
|
185
|
+
8. **You already have an existing memory chunk and want to expose or hide it**
|
|
186
|
+
→ Call `memory_share(chunkId="...", target="agents|hub|both")` or `memory_unshare(chunkId="...", target="agents|hub|all")`.
|
|
187
|
+
|
|
188
|
+
9. **You are about to do anything team-sharing-related**
|
|
189
|
+
→ Call `network_team_info()` first if team server availability is uncertain.
|
|
152
190
|
|
|
153
|
-
|
|
154
|
-
→
|
|
191
|
+
10. **You want to share/stop sharing a skill with local agents or team**
|
|
192
|
+
→ Prefer `skill_publish(skillId="...", target="agents|hub", visibility=...)` and `skill_unpublish(skillId="...", target="agents|hub|all")`.
|
|
155
193
|
|
|
156
|
-
|
|
194
|
+
11. **User asks where to see or manage their memories**
|
|
157
195
|
→ Call `memory_viewer()` and share the URL.
|
|
158
196
|
|
|
159
197
|
## Writing good search queries
|
|
@@ -168,6 +206,6 @@ This skill describes how to use the MemOS memory tools so you can reliably searc
|
|
|
168
206
|
Each memory is tagged with an `owner` (e.g. `agent:main`, `agent:sales-bot`). This is handled **automatically** — you do not need to pass any owner parameter.
|
|
169
207
|
|
|
170
208
|
- **Your memories:** All tools (`memory_search`, `memory_get`, `memory_timeline`) automatically scope queries to your agent's own memories.
|
|
171
|
-
- **
|
|
209
|
+
- **Local shared memories:** Memories marked as local shared are visible to all agents in the same OpenClaw workspace. Use `memory_write_public` to create them, or `memory_share(target='agents')` to expose an existing chunk.
|
|
172
210
|
- **Cross-agent isolation:** You cannot see memories owned by other agents (unless they are public).
|
|
173
211
|
- **How it works:** The system identifies your agent ID from the OpenClaw runtime context and applies owner filtering automatically on every search, recall, and retrieval.
|
package/src/capture/index.ts
CHANGED
|
@@ -33,6 +33,9 @@ const SENTINEL_FAST_RE = new RegExp(
|
|
|
33
33
|
const ENVELOPE_PREFIX_RE =
|
|
34
34
|
/^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+[A-Z]{3}[+-]\d{1,2}\]\s*/;
|
|
35
35
|
|
|
36
|
+
const ENVELOPE_EXTRACT_RE =
|
|
37
|
+
/^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}(?::\d{2})?)\s+([A-Z]{3}[+-]\d{1,2})\]/;
|
|
38
|
+
|
|
36
39
|
/**
|
|
37
40
|
* Extract writable messages from a conversation turn.
|
|
38
41
|
*
|
|
@@ -47,9 +50,11 @@ export function captureMessages(
|
|
|
47
50
|
evidenceTag: string,
|
|
48
51
|
log: Logger,
|
|
49
52
|
owner?: string,
|
|
53
|
+
userSearchTime?: number,
|
|
50
54
|
): ConversationMessage[] {
|
|
51
55
|
const now = Date.now();
|
|
52
56
|
const result: ConversationMessage[] = [];
|
|
57
|
+
let lastTimestamp = 0;
|
|
53
58
|
|
|
54
59
|
for (const msg of messages) {
|
|
55
60
|
const role = msg.role as Role;
|
|
@@ -70,14 +75,24 @@ export function captureMessages(
|
|
|
70
75
|
if (role === "user") {
|
|
71
76
|
content = stripInboundMetadata(content);
|
|
72
77
|
} else {
|
|
78
|
+
content = stripThinkingTags(content);
|
|
73
79
|
content = stripEvidenceWrappers(content, evidenceTag);
|
|
74
80
|
}
|
|
75
81
|
if (!content.trim()) continue;
|
|
76
82
|
|
|
83
|
+
let ts: number;
|
|
84
|
+
if (role === "user" && userSearchTime && userSearchTime > 0) {
|
|
85
|
+
ts = userSearchTime;
|
|
86
|
+
} else {
|
|
87
|
+
ts = now;
|
|
88
|
+
}
|
|
89
|
+
if (ts <= lastTimestamp) ts = lastTimestamp + 1;
|
|
90
|
+
lastTimestamp = ts;
|
|
91
|
+
|
|
77
92
|
result.push({
|
|
78
93
|
role,
|
|
79
94
|
content,
|
|
80
|
-
timestamp:
|
|
95
|
+
timestamp: ts,
|
|
81
96
|
turnId,
|
|
82
97
|
sessionKey,
|
|
83
98
|
toolName: role === "tool" ? msg.toolName : undefined,
|
|
@@ -149,6 +164,30 @@ export function stripInboundMetadata(text: string): string {
|
|
|
149
164
|
return stripEnvelopePrefix(result.join("\n")).trim();
|
|
150
165
|
}
|
|
151
166
|
|
|
167
|
+
/** Strip <think…>…</think> blocks emitted by DeepSeek-style reasoning models. */
|
|
168
|
+
const THINKING_TAG_RE = /<think[\s>][\s\S]*?<\/think>\s*/gi;
|
|
169
|
+
|
|
170
|
+
/** Unwrap <final>…</final> tags from MiniMax-style models (keep content, strip tags). */
|
|
171
|
+
const FINAL_TAG_RE = /<\/?final\s*>/gi;
|
|
172
|
+
|
|
173
|
+
function stripThinkingTags(text: string): string {
|
|
174
|
+
return text.replace(THINKING_TAG_RE, "").replace(FINAL_TAG_RE, "").trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractEnvelopeTimestamp(text: string): number | null {
|
|
178
|
+
const m = ENVELOPE_EXTRACT_RE.exec(text);
|
|
179
|
+
if (!m) return null;
|
|
180
|
+
const [, date, time, tz] = m;
|
|
181
|
+
const timeStr = time.includes(":") && time.split(":").length === 3 ? time : time + ":00";
|
|
182
|
+
const offsetMatch = tz.match(/([+-])(\d{1,2})$/);
|
|
183
|
+
const offsetStr = offsetMatch
|
|
184
|
+
? `${offsetMatch[1]}${offsetMatch[2].padStart(2, "0")}:00`
|
|
185
|
+
: "+00:00";
|
|
186
|
+
const iso = `${date}T${timeStr}${offsetStr}`;
|
|
187
|
+
const ts = new Date(iso).getTime();
|
|
188
|
+
return Number.isNaN(ts) ? null : ts;
|
|
189
|
+
}
|
|
190
|
+
|
|
152
191
|
function stripEnvelopePrefix(text: string): string {
|
|
153
192
|
return text.replace(ENVELOPE_PREFIX_RE, "");
|
|
154
193
|
}
|
package/src/client/connector.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger, MemosLocalConfig } from "../types";
|
|
2
|
-
import type {
|
|
2
|
+
import type { UserRole, UserStatus } from "../sharing/types";
|
|
3
3
|
import type { SqliteStore } from "../storage/sqlite";
|
|
4
4
|
import { hubRequestJson, normalizeHubUrl } from "./hub";
|
|
5
5
|
|
|
@@ -10,6 +10,7 @@ export interface HubSessionInfo {
|
|
|
10
10
|
userToken: string;
|
|
11
11
|
role: UserRole;
|
|
12
12
|
connectedAt: number;
|
|
13
|
+
identityKey?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface HubStatusInfo {
|
|
@@ -20,7 +21,7 @@ export interface HubStatusInfo {
|
|
|
20
21
|
username: string;
|
|
21
22
|
role: UserRole;
|
|
22
23
|
status: UserStatus | string;
|
|
23
|
-
groups:
|
|
24
|
+
groups?: Array<{ id: string; name: string }>;
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -35,6 +36,49 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
35
36
|
|
|
36
37
|
if (!userToken && config.sharing?.client?.teamToken) {
|
|
37
38
|
if (!log) throw new Error("hub client connection is not configured (no userToken, has teamToken but no logger for auto-join)");
|
|
39
|
+
|
|
40
|
+
// If DB has a pending connection (userId exists, no token), check registration-status first
|
|
41
|
+
const persisted = store.getClientHubConnection();
|
|
42
|
+
if (persisted?.userId && !persisted.userToken && hubAddress) {
|
|
43
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
44
|
+
const teamToken = config.sharing.client!.teamToken!;
|
|
45
|
+
try {
|
|
46
|
+
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/registration-status", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: JSON.stringify({ teamToken, userId: persisted.userId }),
|
|
49
|
+
}) as any;
|
|
50
|
+
if (result.status === "active" && result.userToken) {
|
|
51
|
+
log.info(`Pending user approved! Connecting with token. userId=${persisted.userId}`);
|
|
52
|
+
store.setClientHubConnection({
|
|
53
|
+
hubUrl,
|
|
54
|
+
userId: persisted.userId,
|
|
55
|
+
username: persisted.username || "",
|
|
56
|
+
userToken: result.userToken,
|
|
57
|
+
role: "member",
|
|
58
|
+
connectedAt: Date.now(),
|
|
59
|
+
identityKey: persisted.identityKey || "",
|
|
60
|
+
lastKnownStatus: "active",
|
|
61
|
+
});
|
|
62
|
+
return store.getClientHubConnection()!;
|
|
63
|
+
}
|
|
64
|
+
if (result.status === "pending") {
|
|
65
|
+
throw new PendingApprovalError(persisted.userId);
|
|
66
|
+
}
|
|
67
|
+
if (result.status === "rejected") {
|
|
68
|
+
throw new Error("Join request was rejected by the Hub admin.");
|
|
69
|
+
}
|
|
70
|
+
if (result.status === "blocked") {
|
|
71
|
+
throw new Error("Your account has been blocked by the Hub admin.");
|
|
72
|
+
}
|
|
73
|
+
if (result.status === "left" || result.status === "removed") {
|
|
74
|
+
log.info(`User status is "${result.status}", will try to rejoin.`);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof PendingApprovalError) throw err;
|
|
78
|
+
log.warn(`registration-status check failed, falling back to autoJoinHub: ${err}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
return autoJoinHub(store, config, log);
|
|
39
83
|
}
|
|
40
84
|
|
|
@@ -44,6 +88,7 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
44
88
|
|
|
45
89
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
46
90
|
const me = await hubRequestJson(hubUrl, userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
91
|
+
const persisted = store.getClientHubConnection();
|
|
47
92
|
store.setClientHubConnection({
|
|
48
93
|
hubUrl,
|
|
49
94
|
userId: String(me.id),
|
|
@@ -51,22 +96,35 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
51
96
|
userToken,
|
|
52
97
|
role: String(me.role ?? "member") as UserRole,
|
|
53
98
|
connectedAt: Date.now(),
|
|
99
|
+
identityKey: persisted?.identityKey || String(me.identityKey ?? ""),
|
|
100
|
+
lastKnownStatus: "active",
|
|
54
101
|
});
|
|
55
102
|
return store.getClientHubConnection()!;
|
|
56
103
|
}
|
|
57
104
|
|
|
58
105
|
export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig): Promise<HubStatusInfo> {
|
|
59
106
|
const conn = store.getClientHubConnection();
|
|
60
|
-
const
|
|
107
|
+
const configHubAddress = config.sharing?.client?.hubAddress || "";
|
|
108
|
+
const hubAddress = conn?.hubUrl || (configHubAddress ? normalizeHubUrl(configHubAddress) : "");
|
|
61
109
|
const userToken = conn?.userToken || config.sharing?.client?.userToken || "";
|
|
62
110
|
|
|
111
|
+
if (conn && configHubAddress && conn.hubUrl && normalizeHubUrl(configHubAddress) !== conn.hubUrl) {
|
|
112
|
+
store.setClientHubConnection({
|
|
113
|
+
...conn,
|
|
114
|
+
hubUrl: normalizeHubUrl(configHubAddress),
|
|
115
|
+
userToken: "",
|
|
116
|
+
lastKnownStatus: "hub_changed",
|
|
117
|
+
});
|
|
118
|
+
return { connected: false, user: null };
|
|
119
|
+
}
|
|
120
|
+
|
|
63
121
|
if (conn && conn.userId && (!userToken || userToken === "")) {
|
|
64
122
|
const teamToken = config.sharing?.client?.teamToken ?? "";
|
|
65
123
|
if (hubAddress && teamToken) {
|
|
66
124
|
try {
|
|
67
|
-
const result = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/
|
|
125
|
+
const result = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/registration-status", {
|
|
68
126
|
method: "POST",
|
|
69
|
-
body: JSON.stringify({ teamToken,
|
|
127
|
+
body: JSON.stringify({ teamToken, userId: conn.userId }),
|
|
70
128
|
}) as any;
|
|
71
129
|
if (result.status === "pending") {
|
|
72
130
|
return {
|
|
@@ -77,18 +135,19 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
77
135
|
username: conn.username || "",
|
|
78
136
|
role: "member",
|
|
79
137
|
status: "pending",
|
|
80
|
-
groups: [],
|
|
81
138
|
},
|
|
82
139
|
};
|
|
83
140
|
}
|
|
84
141
|
if (result.status === "active" && result.userToken) {
|
|
85
142
|
store.setClientHubConnection({
|
|
86
143
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
87
|
-
userId:
|
|
144
|
+
userId: conn.userId,
|
|
88
145
|
username: conn.username || "",
|
|
89
146
|
userToken: result.userToken,
|
|
90
147
|
role: "member",
|
|
91
148
|
connectedAt: Date.now(),
|
|
149
|
+
identityKey: conn.identityKey || "",
|
|
150
|
+
lastKnownStatus: "active",
|
|
92
151
|
});
|
|
93
152
|
const me = await hubRequestJson(normalizeHubUrl(hubAddress), result.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
94
153
|
return {
|
|
@@ -99,13 +158,6 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
99
158
|
username: String(me.username ?? ""),
|
|
100
159
|
role: String(me.role ?? "member") as UserRole,
|
|
101
160
|
status: String(me.status ?? "active"),
|
|
102
|
-
groups: Array.isArray(me.groups)
|
|
103
|
-
? me.groups.map((group: any) => ({
|
|
104
|
-
id: String(group.id),
|
|
105
|
-
name: String(group.name),
|
|
106
|
-
description: typeof group.description === "string" ? group.description : undefined,
|
|
107
|
-
}))
|
|
108
|
-
: [],
|
|
109
161
|
},
|
|
110
162
|
};
|
|
111
163
|
}
|
|
@@ -118,7 +170,6 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
118
170
|
username: conn.username || "",
|
|
119
171
|
role: "member",
|
|
120
172
|
status: "rejected",
|
|
121
|
-
groups: [],
|
|
122
173
|
},
|
|
123
174
|
};
|
|
124
175
|
}
|
|
@@ -133,24 +184,81 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
133
184
|
|
|
134
185
|
try {
|
|
135
186
|
const me = await hubRequestJson(normalizeHubUrl(hubAddress), userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
187
|
+
const latestUsername = String(me.username ?? "");
|
|
188
|
+
const latestRole = String(me.role ?? "member") as UserRole;
|
|
189
|
+
if (conn && (conn.username !== latestUsername || conn.role !== latestRole)) {
|
|
190
|
+
store.setClientHubConnection({
|
|
191
|
+
...conn,
|
|
192
|
+
username: latestUsername,
|
|
193
|
+
role: latestRole,
|
|
194
|
+
lastKnownStatus: "active",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
136
197
|
return {
|
|
137
198
|
connected: true,
|
|
138
199
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
139
200
|
user: {
|
|
140
201
|
id: String(me.id),
|
|
141
|
-
username:
|
|
142
|
-
role:
|
|
202
|
+
username: latestUsername,
|
|
203
|
+
role: latestRole,
|
|
143
204
|
status: String(me.status ?? "active"),
|
|
144
|
-
groups: Array.isArray(me.groups)
|
|
145
|
-
? me.groups.map((group: any) => ({
|
|
146
|
-
id: String(group.id),
|
|
147
|
-
name: String(group.name),
|
|
148
|
-
description: typeof group.description === "string" ? group.description : undefined,
|
|
149
|
-
}))
|
|
150
|
-
: [],
|
|
205
|
+
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
151
206
|
},
|
|
152
207
|
};
|
|
153
|
-
} catch {
|
|
208
|
+
} catch (err: any) {
|
|
209
|
+
const is401 = typeof err?.message === "string" && err.message.includes("(401)");
|
|
210
|
+
if (is401 && conn) {
|
|
211
|
+
const teamToken = config.sharing?.client?.teamToken ?? "";
|
|
212
|
+
if (hubAddress && teamToken) {
|
|
213
|
+
try {
|
|
214
|
+
const regResult = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/registration-status", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: JSON.stringify({ teamToken, userId: conn.userId }),
|
|
217
|
+
}) as any;
|
|
218
|
+
if (regResult.status === "active" && regResult.userToken) {
|
|
219
|
+
store.setClientHubConnection({
|
|
220
|
+
...conn,
|
|
221
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
222
|
+
userToken: regResult.userToken,
|
|
223
|
+
connectedAt: Date.now(),
|
|
224
|
+
lastKnownStatus: "active",
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
const me = await hubRequestJson(normalizeHubUrl(hubAddress), regResult.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
228
|
+
return {
|
|
229
|
+
connected: true,
|
|
230
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
231
|
+
user: {
|
|
232
|
+
id: String(me.id),
|
|
233
|
+
username: String(me.username ?? ""),
|
|
234
|
+
role: String(me.role ?? "member") as UserRole,
|
|
235
|
+
status: String(me.status ?? "active"),
|
|
236
|
+
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
} catch { /* fall through to token-only return */ }
|
|
240
|
+
return {
|
|
241
|
+
connected: true,
|
|
242
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
243
|
+
user: { id: conn.userId, username: conn.username || "", role: conn.role as UserRole || "member", status: "active" },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const realStatus = regResult.status as string;
|
|
247
|
+
store.setClientHubConnection({ ...conn, userToken: "", lastKnownStatus: realStatus });
|
|
248
|
+
return {
|
|
249
|
+
connected: false,
|
|
250
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
251
|
+
user: { id: conn.userId, username: conn.username || "", role: "member", status: realStatus },
|
|
252
|
+
};
|
|
253
|
+
} catch { /* registration-status also failed, fall through */ }
|
|
254
|
+
}
|
|
255
|
+
store.setClientHubConnection({ ...conn, userToken: "", lastKnownStatus: "token_expired" });
|
|
256
|
+
return {
|
|
257
|
+
connected: false,
|
|
258
|
+
hubUrl: normalizeHubUrl(hubAddress),
|
|
259
|
+
user: { id: conn.userId, username: conn.username || "", role: "member", status: "token_expired" },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
154
262
|
return { connected: false, user: null };
|
|
155
263
|
}
|
|
156
264
|
}
|
|
@@ -166,15 +274,32 @@ export async function autoJoinHub(
|
|
|
166
274
|
throw new Error("hubAddress and teamToken are required for auto-join");
|
|
167
275
|
}
|
|
168
276
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
169
|
-
const
|
|
170
|
-
const
|
|
277
|
+
const osModule = typeof globalThis.process !== "undefined" ? await import("os") : null;
|
|
278
|
+
const hostname = osModule ? osModule.hostname() : "unknown";
|
|
279
|
+
const nickname = config.sharing?.client?.nickname;
|
|
280
|
+
const username = nickname || (osModule ? osModule.userInfo().username : "user");
|
|
281
|
+
let clientIp = "";
|
|
282
|
+
if (osModule) {
|
|
283
|
+
const nets = osModule.networkInterfaces();
|
|
284
|
+
for (const name of Object.keys(nets)) {
|
|
285
|
+
for (const net of nets[name] ?? []) {
|
|
286
|
+
if (net.family === "IPv4" && !net.internal) { clientIp = net.address; break; }
|
|
287
|
+
}
|
|
288
|
+
if (clientIp) break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const persisted = store.getClientHubConnection();
|
|
293
|
+
const existingIdentityKey = persisted?.identityKey || "";
|
|
171
294
|
|
|
172
295
|
log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
|
|
173
296
|
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
174
297
|
method: "POST",
|
|
175
|
-
body: JSON.stringify({ teamToken, username, deviceName: hostname }),
|
|
298
|
+
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp, identityKey: existingIdentityKey }),
|
|
176
299
|
}) as any;
|
|
177
300
|
|
|
301
|
+
const returnedIdentityKey = String(result.identityKey || existingIdentityKey || "");
|
|
302
|
+
|
|
178
303
|
if (result.status === "pending") {
|
|
179
304
|
log.info(`Join request submitted, awaiting admin approval. userId=${result.userId}`);
|
|
180
305
|
store.setClientHubConnection({
|
|
@@ -184,6 +309,8 @@ export async function autoJoinHub(
|
|
|
184
309
|
userToken: "",
|
|
185
310
|
role: "member",
|
|
186
311
|
connectedAt: Date.now(),
|
|
312
|
+
identityKey: returnedIdentityKey,
|
|
313
|
+
lastKnownStatus: "pending",
|
|
187
314
|
});
|
|
188
315
|
throw new PendingApprovalError(result.userId);
|
|
189
316
|
}
|
|
@@ -192,6 +319,10 @@ export async function autoJoinHub(
|
|
|
192
319
|
throw new Error(`Join request was rejected by the Hub admin.`);
|
|
193
320
|
}
|
|
194
321
|
|
|
322
|
+
if (result.status === "blocked") {
|
|
323
|
+
throw new Error(`Your account has been blocked by the Hub admin.`);
|
|
324
|
+
}
|
|
325
|
+
|
|
195
326
|
if (!result.userToken) {
|
|
196
327
|
throw new Error(`Hub join failed: ${JSON.stringify(result)}`);
|
|
197
328
|
}
|
|
@@ -204,6 +335,8 @@ export async function autoJoinHub(
|
|
|
204
335
|
userToken: result.userToken,
|
|
205
336
|
role: "member",
|
|
206
337
|
connectedAt: Date.now(),
|
|
338
|
+
identityKey: returnedIdentityKey,
|
|
339
|
+
lastKnownStatus: "active",
|
|
207
340
|
});
|
|
208
341
|
return store.getClientHubConnection()!;
|
|
209
342
|
}
|
package/src/client/hub.ts
CHANGED
|
@@ -157,16 +157,34 @@ export async function hubUpdateUsername(
|
|
|
157
157
|
return result;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
let _cachedClientIp: string | null = null;
|
|
161
|
+
function getClientIp(): string {
|
|
162
|
+
if (_cachedClientIp !== null) return _cachedClientIp;
|
|
163
|
+
try {
|
|
164
|
+
const os = require("os");
|
|
165
|
+
const nets = os.networkInterfaces();
|
|
166
|
+
for (const name of Object.keys(nets)) {
|
|
167
|
+
for (const net of nets[name] ?? []) {
|
|
168
|
+
if (net.family === "IPv4" && !net.internal) { _cachedClientIp = net.address; return _cachedClientIp!; }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch { /* browser or no os module */ }
|
|
172
|
+
_cachedClientIp = "";
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
export async function hubRequestJson(
|
|
161
177
|
hubUrl: string,
|
|
162
178
|
userToken: string,
|
|
163
179
|
route: string,
|
|
164
180
|
init: RequestInit = {},
|
|
165
181
|
): Promise<unknown> {
|
|
182
|
+
const clientIp = getClientIp();
|
|
166
183
|
const res = await fetch(`${normalizeHubUrl(hubUrl)}${route}`, {
|
|
167
184
|
...init,
|
|
168
185
|
headers: {
|
|
169
186
|
authorization: `Bearer ${userToken}`,
|
|
187
|
+
...(clientIp ? { "x-client-ip": clientIp } : {}),
|
|
170
188
|
...(init.body ? { "content-type": "application/json" } : {}),
|
|
171
189
|
...(init.headers ?? {}),
|
|
172
190
|
},
|