@sonicbotman/lobster-press 3.2.4 → 3.2.6
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,207 @@
|
|
|
1
|
+
# LobsterPress MCP Server
|
|
2
|
+
|
|
3
|
+
> **推荐方式**:如果你在使用 OpenClaw,请优先使用
|
|
4
|
+
> [插件安装模式](../README.md#-openclaw-插件推荐),
|
|
5
|
+
> 更简单,无需手动管理进程。
|
|
6
|
+
>
|
|
7
|
+
> 本文档描述直接运行 MCP Server 的高级用法,适合非 OpenClaw 环境
|
|
8
|
+
> 或需要自定义集成的场景。
|
|
9
|
+
|
|
10
|
+
基于 Model Context Protocol (MCP) 的 OpenClaw 会话压缩服务。
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd lobster-press/mcp_server
|
|
16
|
+
pip install -r requirements.txt
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 使用方法
|
|
20
|
+
|
|
21
|
+
### 1. 与 Claude Desktop 集成
|
|
22
|
+
|
|
23
|
+
在 Claude Desktop 配置文件中添加:
|
|
24
|
+
|
|
25
|
+
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
26
|
+
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"lobster-press": {
|
|
32
|
+
"command": "python3",
|
|
33
|
+
"args": ["/path/to/lobster-press/mcp_server/lobster_mcp_server.py"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. 与 Cursor 集成
|
|
40
|
+
|
|
41
|
+
在 Cursor 设置中添加 MCP 服务器配置。
|
|
42
|
+
|
|
43
|
+
### 3. 测试模式
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 lobster_mcp_server.py --test
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 可用工具
|
|
50
|
+
|
|
51
|
+
### 1. compress_session
|
|
52
|
+
|
|
53
|
+
压缩 OpenClaw 会话历史。
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"session_id": "abc123",
|
|
58
|
+
"strategy": "medium",
|
|
59
|
+
"dry_run": false
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**参数:**
|
|
64
|
+
- `session_id`: 会话 ID(文件名,不含扩展名)
|
|
65
|
+
- `strategy`: 压缩策略(light/medium/aggressive)
|
|
66
|
+
- `dry_run`: 是否仅预览(默认 false)
|
|
67
|
+
|
|
68
|
+
**返回:**
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"status": "success",
|
|
72
|
+
"original_messages": 1000,
|
|
73
|
+
"compressed_messages": 500,
|
|
74
|
+
"tokens_saved": 50000,
|
|
75
|
+
"compression_ratio": "50%"
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. preview_compression
|
|
80
|
+
|
|
81
|
+
预览压缩效果。
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"session_id": "abc123",
|
|
86
|
+
"strategy": "medium"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. get_compression_stats
|
|
91
|
+
|
|
92
|
+
获取压缩统计数据。
|
|
93
|
+
|
|
94
|
+
### 4. update_weights
|
|
95
|
+
|
|
96
|
+
更新消息类型权重配置。
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"weights": {
|
|
101
|
+
"decision": 0.3,
|
|
102
|
+
"error": 0.25,
|
|
103
|
+
"config": 0.2,
|
|
104
|
+
"preference": 0.15
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 5. list_sessions
|
|
110
|
+
|
|
111
|
+
列出所有可压缩的会话。
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"min_tokens": 10000
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 压缩策略
|
|
120
|
+
|
|
121
|
+
| 策略 | 保留比例 | 适用场景 |
|
|
122
|
+
|------|---------|---------|
|
|
123
|
+
| light | 70% | 保留大部分信息 |
|
|
124
|
+
| medium | 50% | 平衡压缩与保留 |
|
|
125
|
+
| aggressive | 30% | 最大压缩 |
|
|
126
|
+
|
|
127
|
+
## 消息评分
|
|
128
|
+
|
|
129
|
+
消息按以下权重评分:
|
|
130
|
+
|
|
131
|
+
| 类型 | 权重 | 示例 |
|
|
132
|
+
|------|------|------|
|
|
133
|
+
| decision | 0.3 | "决定采用方案A" |
|
|
134
|
+
| error | 0.25 | "发生错误:连接超时" |
|
|
135
|
+
| config | 0.2 | "API Key 已更新" |
|
|
136
|
+
| preference | 0.15 | "用户偏好使用 GLM-4" |
|
|
137
|
+
| context | 0.05 | "当前系统版本 2026.3.2" |
|
|
138
|
+
| chitchat | 0.02 | "哈哈,不错" |
|
|
139
|
+
|
|
140
|
+
## 资源访问
|
|
141
|
+
|
|
142
|
+
MCP 客户端可以通过以下 URI 访问会话资源:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
lobster://sessions/{session_id}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 示例
|
|
149
|
+
|
|
150
|
+
### Python 客户端
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import json
|
|
154
|
+
import subprocess
|
|
155
|
+
|
|
156
|
+
def call_mcp_tool(tool_name, arguments):
|
|
157
|
+
request = {
|
|
158
|
+
"method": "tools/call",
|
|
159
|
+
"params": {
|
|
160
|
+
"name": tool_name,
|
|
161
|
+
"arguments": arguments
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
proc = subprocess.Popen(
|
|
166
|
+
["python3", "lobster_mcp_server.py"],
|
|
167
|
+
stdin=subprocess.PIPE,
|
|
168
|
+
stdout=subprocess.PIPE,
|
|
169
|
+
text=True
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
stdout, _ = proc.communicate(json.dumps(request))
|
|
173
|
+
return json.loads(stdout)
|
|
174
|
+
|
|
175
|
+
# 列出会话
|
|
176
|
+
result = call_mcp_tool("list_sessions", {"min_tokens": 10000})
|
|
177
|
+
print(result)
|
|
178
|
+
|
|
179
|
+
# 预览压缩
|
|
180
|
+
result = call_mcp_tool("preview_compression", {
|
|
181
|
+
"session_id": "abc123",
|
|
182
|
+
"strategy": "medium"
|
|
183
|
+
})
|
|
184
|
+
print(result)
|
|
185
|
+
|
|
186
|
+
# 执行压缩
|
|
187
|
+
result = call_mcp_tool("compress_session", {
|
|
188
|
+
"session_id": "abc123",
|
|
189
|
+
"strategy": "medium"
|
|
190
|
+
})
|
|
191
|
+
print(result)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 安全性
|
|
195
|
+
|
|
196
|
+
- ✅ 自动备份原文件(带时间戳)
|
|
197
|
+
- ✅ 仅本地操作,不上传数据
|
|
198
|
+
- ✅ 支持预览模式(dry_run)
|
|
199
|
+
|
|
200
|
+
## 许可证
|
|
201
|
+
|
|
202
|
+
MIT License
|
|
203
|
+
|
|
204
|
+
## 相关
|
|
205
|
+
|
|
206
|
+
- Issue #42: v2.0 架构演进:向 MCP Server 演进
|
|
207
|
+
- Issue #28: MCP 协议支持
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lobster-press",
|
|
3
|
+
"version": "1.2.8",
|
|
4
|
+
"description": "LobsterPress - 智能 OpenClaw 会话压缩服务 (MCP Server)",
|
|
5
|
+
"main": "lobster_mcp_server.py",
|
|
6
|
+
"type": "mcp-server",
|
|
7
|
+
"tools": [
|
|
8
|
+
{
|
|
9
|
+
"name": "compress_session",
|
|
10
|
+
"description": "压缩 OpenClaw 会话历史,保留重要信息"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "preview_compression",
|
|
14
|
+
"description": "预览压缩效果,显示将要保留和删除的消息"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "get_compression_stats",
|
|
18
|
+
"description": "获取压缩统计数据"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "update_weights",
|
|
22
|
+
"description": "更新消息类型权重配置"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "list_sessions",
|
|
26
|
+
"description": "列出所有可压缩的会话"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"resources": [
|
|
30
|
+
{
|
|
31
|
+
"uri": "lobster://sessions/{session_id}",
|
|
32
|
+
"description": "访问 OpenClaw 会话数据"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"config": {
|
|
36
|
+
"sessions_dir": "~/.openclaw/agents/main/sessions",
|
|
37
|
+
"default_strategy": "medium",
|
|
38
|
+
"strategies": {
|
|
39
|
+
"light": {
|
|
40
|
+
"retention_ratio": 0.7,
|
|
41
|
+
"description": "轻度压缩,保留 70% 消息"
|
|
42
|
+
},
|
|
43
|
+
"medium": {
|
|
44
|
+
"retention_ratio": 0.5,
|
|
45
|
+
"description": "中度压缩,保留 50% 消息"
|
|
46
|
+
},
|
|
47
|
+
"aggressive": {
|
|
48
|
+
"retention_ratio": 0.3,
|
|
49
|
+
"description": "激进压缩,保留 30% 消息"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"author": "LobsterPress Team",
|
|
54
|
+
"repository": "https://github.com/SonicBotMan/lobster-press",
|
|
55
|
+
"license": "MIT"
|
|
56
|
+
}
|
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
LobsterPress MCP Server v1.0.0
|
|
5
|
+
基于 Model Context Protocol (MCP) 的压缩服务
|
|
6
|
+
|
|
7
|
+
Issue: #42 - v2.0 架构演进:向 MCP Server 演进
|
|
8
|
+
Author: LobsterPress Team
|
|
9
|
+
Version: v1.3.0
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import json
|
|
14
|
+
import asyncio
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, List, Optional, Any
|
|
19
|
+
from dataclasses import dataclass, asdict
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class MCPTool:
|
|
25
|
+
"""MCP 工具定义"""
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
input_schema: Dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LobsterPressMCPServer:
|
|
32
|
+
"""LobsterPress MCP Server"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, sessions_dir: str = None, db_path: str = None, llm_provider: str = None, llm_model: str = None):
|
|
35
|
+
"""初始化 MCP Server
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
sessions_dir: 会话目录
|
|
39
|
+
db_path: LobsterPress 数据库路径
|
|
40
|
+
llm_provider: LLM 提供商
|
|
41
|
+
llm_model: LLM 模型名称
|
|
42
|
+
"""
|
|
43
|
+
self.sessions_dir = Path(sessions_dir or os.path.expanduser("~/.openclaw/agents/main/sessions"))
|
|
44
|
+
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
# v3.2.2: OpenClaw 插件支持
|
|
47
|
+
self.db_path = db_path or os.path.expanduser("~/.openclaw/lobster.db")
|
|
48
|
+
self.llm_provider = llm_provider
|
|
49
|
+
self.llm_model = llm_model
|
|
50
|
+
self._db = None # 懒加载数据库连接
|
|
51
|
+
|
|
52
|
+
# 统计数据
|
|
53
|
+
self.stats = {
|
|
54
|
+
"total_compressions": 0,
|
|
55
|
+
"total_messages_processed": 0,
|
|
56
|
+
"total_tokens_saved": 0,
|
|
57
|
+
"last_compression": None
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# 配置
|
|
61
|
+
self.config = {
|
|
62
|
+
"weights": {
|
|
63
|
+
"decision": 0.3,
|
|
64
|
+
"error": 0.25,
|
|
65
|
+
"config": 0.2,
|
|
66
|
+
"preference": 0.15,
|
|
67
|
+
"context": 0.05,
|
|
68
|
+
"chitchat": 0.02,
|
|
69
|
+
"other": 0.03
|
|
70
|
+
},
|
|
71
|
+
"strategy": "medium",
|
|
72
|
+
"max_tokens": 800000
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# 注册工具
|
|
76
|
+
self.tools = self._register_tools()
|
|
77
|
+
|
|
78
|
+
def _register_tools(self) -> List[MCPTool]:
|
|
79
|
+
"""注册 MCP 工具"""
|
|
80
|
+
return [
|
|
81
|
+
MCPTool(
|
|
82
|
+
name="compress_session",
|
|
83
|
+
description="压缩 OpenClaw 会话历史,保留重要信息",
|
|
84
|
+
input_schema={
|
|
85
|
+
"type": "object",
|
|
86
|
+
"properties": {
|
|
87
|
+
"session_id": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "会话 ID(文件名,不含扩展名)"
|
|
90
|
+
},
|
|
91
|
+
"strategy": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": ["light", "medium", "aggressive"],
|
|
94
|
+
"description": "压缩策略:light(轻度)、medium(中度)、aggressive(激进)",
|
|
95
|
+
"default": "medium"
|
|
96
|
+
},
|
|
97
|
+
"dry_run": {
|
|
98
|
+
"type": "boolean",
|
|
99
|
+
"description": "是否仅预览,不实际压缩",
|
|
100
|
+
"default": False
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"required": ["session_id"]
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
MCPTool(
|
|
107
|
+
name="preview_compression",
|
|
108
|
+
description="预览压缩效果,显示将要保留和删除的消息",
|
|
109
|
+
input_schema={
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"session_id": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"description": "会话 ID"
|
|
115
|
+
},
|
|
116
|
+
"strategy": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"enum": ["light", "medium", "aggressive"],
|
|
119
|
+
"default": "medium"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"required": ["session_id"]
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
MCPTool(
|
|
126
|
+
name="get_compression_stats",
|
|
127
|
+
description="获取压缩统计数据",
|
|
128
|
+
input_schema={
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {},
|
|
131
|
+
"required": []
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
MCPTool(
|
|
135
|
+
name="update_weights",
|
|
136
|
+
description="更新消息类型权重配置",
|
|
137
|
+
input_schema={
|
|
138
|
+
"type": "object",
|
|
139
|
+
"properties": {
|
|
140
|
+
"weights": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"description": "消息类型权重",
|
|
143
|
+
"properties": {
|
|
144
|
+
"decision": {"type": "number"},
|
|
145
|
+
"error": {"type": "number"},
|
|
146
|
+
"config": {"type": "number"},
|
|
147
|
+
"preference": {"type": "number"},
|
|
148
|
+
"context": {"type": "number"},
|
|
149
|
+
"chitchat": {"type": "number"}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"required": ["weights"]
|
|
154
|
+
}
|
|
155
|
+
),
|
|
156
|
+
MCPTool(
|
|
157
|
+
name="list_sessions",
|
|
158
|
+
description="列出所有可压缩的会话",
|
|
159
|
+
input_schema={
|
|
160
|
+
"type": "object",
|
|
161
|
+
"properties": {
|
|
162
|
+
"min_tokens": {
|
|
163
|
+
"type": "integer",
|
|
164
|
+
"description": "最小 Token 数阈值",
|
|
165
|
+
"default": 10000
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"required": []
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
# v3.2.2: OpenClaw 插件工具
|
|
172
|
+
MCPTool(
|
|
173
|
+
name="lobster_grep",
|
|
174
|
+
description="在 LobsterPress 记忆库中全文搜索历史对话(FTS5 + TF-IDF 重排序)",
|
|
175
|
+
input_schema={
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {
|
|
178
|
+
"query": {"type": "string", "description": "搜索关键词或短语"},
|
|
179
|
+
"conversation_id": {"type": "string", "description": "限定搜索范围的会话 ID(可选)"},
|
|
180
|
+
"limit": {"type": "integer", "description": "最多返回条数,默认 5", "default": 5}
|
|
181
|
+
},
|
|
182
|
+
"required": ["query"]
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
MCPTool(
|
|
186
|
+
name="lobster_describe",
|
|
187
|
+
description="查看 LobsterPress 的 DAG 摘要层级结构",
|
|
188
|
+
input_schema={
|
|
189
|
+
"type": "object",
|
|
190
|
+
"properties": {
|
|
191
|
+
"conversation_id": {"type": "string", "description": "会话 ID(可选,留空查全局)"}
|
|
192
|
+
},
|
|
193
|
+
"required": []
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
MCPTool(
|
|
197
|
+
name="lobster_expand",
|
|
198
|
+
description="将 DAG 摘要节点展开,还原其对应的原始消息(无损检索)",
|
|
199
|
+
input_schema={
|
|
200
|
+
"type": "object",
|
|
201
|
+
"properties": {
|
|
202
|
+
"summary_id": {"type": "string", "description": "要展开的摘要节点 ID"},
|
|
203
|
+
"max_depth": {"type": "integer", "description": "最大展开层数,默认 2", "default": 2}
|
|
204
|
+
},
|
|
205
|
+
"required": ["summary_id"]
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
211
|
+
"""处理 MCP 请求
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
request: MCP 请求
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
MCP 响应
|
|
218
|
+
"""
|
|
219
|
+
method = request.get("method")
|
|
220
|
+
params = request.get("params", {})
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
if method == "tools/list":
|
|
224
|
+
return {
|
|
225
|
+
"tools": [asdict(tool) for tool in self.tools]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
elif method == "tools/call":
|
|
229
|
+
tool_name = params.get("name")
|
|
230
|
+
arguments = params.get("arguments", {})
|
|
231
|
+
|
|
232
|
+
result = await self._call_tool(tool_name, arguments)
|
|
233
|
+
return {
|
|
234
|
+
"content": [
|
|
235
|
+
{
|
|
236
|
+
"type": "text",
|
|
237
|
+
"text": json.dumps(result, ensure_ascii=False, indent=2)
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
elif method == "resources/list":
|
|
243
|
+
return await self._list_resources()
|
|
244
|
+
|
|
245
|
+
elif method == "resources/read":
|
|
246
|
+
return await self._read_resource(params.get("uri"))
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
return {"error": f"Unknown method: {method}"}
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
return {"error": str(e)}
|
|
253
|
+
|
|
254
|
+
async def _call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
255
|
+
"""调用工具"""
|
|
256
|
+
if tool_name == "compress_session":
|
|
257
|
+
return await self._compress_session(
|
|
258
|
+
arguments.get("session_id"),
|
|
259
|
+
arguments.get("strategy", "medium"),
|
|
260
|
+
arguments.get("dry_run", False)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
elif tool_name == "preview_compression":
|
|
264
|
+
return await self._preview_compression(
|
|
265
|
+
arguments.get("session_id"),
|
|
266
|
+
arguments.get("strategy", "medium")
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
elif tool_name == "get_compression_stats":
|
|
270
|
+
return self._get_stats()
|
|
271
|
+
|
|
272
|
+
elif tool_name == "update_weights":
|
|
273
|
+
return self._update_weights(arguments.get("weights", {}))
|
|
274
|
+
|
|
275
|
+
elif tool_name == "list_sessions":
|
|
276
|
+
return await self._list_sessions(arguments.get("min_tokens", 10000))
|
|
277
|
+
|
|
278
|
+
# v3.2.2: OpenClaw 插件工具
|
|
279
|
+
elif tool_name == "lobster_grep":
|
|
280
|
+
from agent_tools import lobster_grep
|
|
281
|
+
db = self._get_db()
|
|
282
|
+
results = lobster_grep(
|
|
283
|
+
db,
|
|
284
|
+
arguments["query"],
|
|
285
|
+
conversation_id=arguments.get("conversation_id"),
|
|
286
|
+
limit=arguments.get("limit", 5)
|
|
287
|
+
)
|
|
288
|
+
return {"results": results}
|
|
289
|
+
|
|
290
|
+
elif tool_name == "lobster_describe":
|
|
291
|
+
from agent_tools import lobster_describe
|
|
292
|
+
db = self._get_db()
|
|
293
|
+
return lobster_describe(db, conversation_id=arguments.get("conversation_id"))
|
|
294
|
+
|
|
295
|
+
elif tool_name == "lobster_expand":
|
|
296
|
+
from agent_tools import lobster_expand
|
|
297
|
+
db = self._get_db()
|
|
298
|
+
return lobster_expand(db, arguments["summary_id"])
|
|
299
|
+
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError(f"Unknown tool: {tool_name}")
|
|
302
|
+
|
|
303
|
+
def _get_db(self):
|
|
304
|
+
"""获取数据库连接(懒加载)"""
|
|
305
|
+
if self._db is None:
|
|
306
|
+
# 添加 src 目录到 path
|
|
307
|
+
src_dir = Path(__file__).parent.parent / "src"
|
|
308
|
+
if str(src_dir) not in sys.path:
|
|
309
|
+
sys.path.insert(0, str(src_dir))
|
|
310
|
+
|
|
311
|
+
from database import LobsterDatabase
|
|
312
|
+
self._db = LobsterDatabase(self.db_path)
|
|
313
|
+
return self._db
|
|
314
|
+
|
|
315
|
+
def _validate_session_id(self, session_id: str) -> str:
|
|
316
|
+
"""验证 session_id 防止路径遍历攻击(修复 Issue #12)
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
session_id: 会话 ID
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
验证后的会话 ID
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
ValueError: 无效的会话 ID
|
|
326
|
+
"""
|
|
327
|
+
if not session_id:
|
|
328
|
+
raise ValueError("Session ID cannot be empty")
|
|
329
|
+
|
|
330
|
+
# 只允许字母、数字、下划线、连字符
|
|
331
|
+
if not re.match(r'^[a-zA-Z0-9_-]+$', session_id):
|
|
332
|
+
raise ValueError(f"Invalid session_id: {session_id} (only alphanumeric, underscore, hyphen allowed)")
|
|
333
|
+
|
|
334
|
+
# 防止路径遍历
|
|
335
|
+
if '..' in session_id or '/' in session_id or '\\' in session_id:
|
|
336
|
+
raise ValueError(f"Invalid session_id: {session_id} (path traversal detected)")
|
|
337
|
+
|
|
338
|
+
# 长度限制
|
|
339
|
+
if len(session_id) > 255:
|
|
340
|
+
raise ValueError(f"Session ID too long: {len(session_id)} > 255")
|
|
341
|
+
|
|
342
|
+
return session_id
|
|
343
|
+
|
|
344
|
+
async def _compress_session(self, session_id: str, strategy: str, dry_run: bool) -> Dict[str, Any]:
|
|
345
|
+
"""压缩会话"""
|
|
346
|
+
# 验证 session_id(修复 Issue #12)
|
|
347
|
+
session_id = self._validate_session_id(session_id)
|
|
348
|
+
session_file = self.sessions_dir / f"{session_id}.jsonl"
|
|
349
|
+
|
|
350
|
+
if not session_file.exists():
|
|
351
|
+
raise FileNotFoundError(f"Session not found: {session_id}")
|
|
352
|
+
|
|
353
|
+
# 读取会话
|
|
354
|
+
messages = []
|
|
355
|
+
with open(session_file, 'r', encoding='utf-8') as f:
|
|
356
|
+
for line in f:
|
|
357
|
+
if line.strip():
|
|
358
|
+
messages.append(json.loads(line))
|
|
359
|
+
|
|
360
|
+
total_messages = len(messages)
|
|
361
|
+
|
|
362
|
+
# 估算 Token 数
|
|
363
|
+
estimated_tokens = self._estimate_tokens(messages)
|
|
364
|
+
|
|
365
|
+
# 根据策略决定保留比例
|
|
366
|
+
retention_ratios = {
|
|
367
|
+
"light": 0.7,
|
|
368
|
+
"medium": 0.5,
|
|
369
|
+
"aggressive": 0.3
|
|
370
|
+
}
|
|
371
|
+
retention_ratio = retention_ratios.get(strategy, 0.5)
|
|
372
|
+
|
|
373
|
+
# 计算要保留的消息数
|
|
374
|
+
keep_count = int(total_messages * retention_ratio)
|
|
375
|
+
|
|
376
|
+
# 评分并排序(简化版)
|
|
377
|
+
scored_messages = []
|
|
378
|
+
for i, msg in enumerate(messages):
|
|
379
|
+
score = self._score_message(msg)
|
|
380
|
+
scored_messages.append((i, score, msg))
|
|
381
|
+
|
|
382
|
+
# 按分数排序,保留高分消息
|
|
383
|
+
scored_messages.sort(key=lambda x: x[1], reverse=True)
|
|
384
|
+
keep_indices = set(x[0] for x in scored_messages[:keep_count])
|
|
385
|
+
|
|
386
|
+
# 按原始顺序保留
|
|
387
|
+
compressed_messages = [msg for i, msg in enumerate(messages) if i in keep_indices]
|
|
388
|
+
|
|
389
|
+
# 计算压缩后的 Token 数
|
|
390
|
+
compressed_tokens = self._estimate_tokens(compressed_messages)
|
|
391
|
+
tokens_saved = estimated_tokens - compressed_tokens
|
|
392
|
+
|
|
393
|
+
if dry_run:
|
|
394
|
+
return {
|
|
395
|
+
"status": "preview",
|
|
396
|
+
"session_id": session_id,
|
|
397
|
+
"original_messages": total_messages,
|
|
398
|
+
"compressed_messages": len(compressed_messages),
|
|
399
|
+
"original_tokens": estimated_tokens,
|
|
400
|
+
"compressed_tokens": compressed_tokens,
|
|
401
|
+
"tokens_saved": tokens_saved,
|
|
402
|
+
"compression_ratio": f"{(tokens_saved / estimated_tokens * 100):.1f}%" if estimated_tokens > 0 else "0%",
|
|
403
|
+
"strategy": strategy
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# 实际写入(备份原文件)
|
|
407
|
+
backup_file = self.sessions_dir / f"{session_id}.backup.{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
408
|
+
session_file.rename(backup_file)
|
|
409
|
+
|
|
410
|
+
with open(session_file, 'w', encoding='utf-8') as f:
|
|
411
|
+
for msg in compressed_messages:
|
|
412
|
+
f.write(json.dumps(msg, ensure_ascii=False) + '\n')
|
|
413
|
+
|
|
414
|
+
# 更新统计
|
|
415
|
+
self.stats["total_compressions"] += 1
|
|
416
|
+
self.stats["total_messages_processed"] += total_messages
|
|
417
|
+
self.stats["total_tokens_saved"] += tokens_saved
|
|
418
|
+
self.stats["last_compression"] = datetime.now().isoformat()
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
"status": "success",
|
|
422
|
+
"session_id": session_id,
|
|
423
|
+
"original_messages": total_messages,
|
|
424
|
+
"compressed_messages": len(compressed_messages),
|
|
425
|
+
"original_tokens": estimated_tokens,
|
|
426
|
+
"compressed_tokens": compressed_tokens,
|
|
427
|
+
"tokens_saved": tokens_saved,
|
|
428
|
+
"compression_ratio": f"{(tokens_saved / estimated_tokens * 100):.1f}%" if estimated_tokens > 0 else "0%",
|
|
429
|
+
"strategy": strategy,
|
|
430
|
+
"backup_file": str(backup_file)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async def _preview_compression(self, session_id: str, strategy: str) -> Dict[str, Any]:
|
|
434
|
+
"""预览压缩效果"""
|
|
435
|
+
return await self._compress_session(session_id, strategy, dry_run=True)
|
|
436
|
+
|
|
437
|
+
def _get_stats(self) -> Dict[str, Any]:
|
|
438
|
+
"""获取统计数据"""
|
|
439
|
+
return {
|
|
440
|
+
"stats": self.stats,
|
|
441
|
+
"config": self.config
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
def _update_weights(self, weights: Dict[str, float]) -> Dict[str, Any]:
|
|
445
|
+
"""更新权重配置"""
|
|
446
|
+
# 验证权重
|
|
447
|
+
for key, value in weights.items():
|
|
448
|
+
if key in self.config["weights"]:
|
|
449
|
+
self.config["weights"][key] = value
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
"status": "success",
|
|
453
|
+
"updated_weights": self.config["weights"]
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async def _list_sessions(self, min_tokens: int) -> Dict[str, Any]:
|
|
457
|
+
"""列出会话"""
|
|
458
|
+
sessions = []
|
|
459
|
+
|
|
460
|
+
for session_file in self.sessions_dir.glob("*.jsonl"):
|
|
461
|
+
if session_file.name.endswith((".backup.", ".reset.", ".deleted.")):
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
# 估算 Token 数
|
|
465
|
+
messages = []
|
|
466
|
+
with open(session_file, 'r', encoding='utf-8') as f:
|
|
467
|
+
for line in f:
|
|
468
|
+
if line.strip():
|
|
469
|
+
messages.append(json.loads(line))
|
|
470
|
+
|
|
471
|
+
estimated_tokens = self._estimate_tokens(messages)
|
|
472
|
+
|
|
473
|
+
if estimated_tokens >= min_tokens:
|
|
474
|
+
sessions.append({
|
|
475
|
+
"session_id": session_file.stem,
|
|
476
|
+
"messages": len(messages),
|
|
477
|
+
"estimated_tokens": estimated_tokens,
|
|
478
|
+
"file_size": session_file.stat().st_size,
|
|
479
|
+
"modified": datetime.fromtimestamp(session_file.stat().st_mtime).isoformat()
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
# 按 Token 数排序
|
|
483
|
+
sessions.sort(key=lambda x: x["estimated_tokens"], reverse=True)
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
"sessions": sessions,
|
|
487
|
+
"total": len(sessions)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async def _list_resources(self) -> Dict[str, Any]:
|
|
491
|
+
"""列出资源"""
|
|
492
|
+
resources = []
|
|
493
|
+
|
|
494
|
+
for session_file in self.sessions_dir.glob("*.jsonl"):
|
|
495
|
+
if not session_file.name.endswith((".backup.", ".reset.", ".deleted.")):
|
|
496
|
+
resources.append({
|
|
497
|
+
"uri": f"lobster://sessions/{session_file.stem}",
|
|
498
|
+
"name": session_file.stem,
|
|
499
|
+
"mimeType": "application/jsonl"
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
return {"resources": resources}
|
|
503
|
+
|
|
504
|
+
async def _read_resource(self, uri: str) -> Dict[str, Any]:
|
|
505
|
+
"""读取资源"""
|
|
506
|
+
if not uri.startswith("lobster://sessions/"):
|
|
507
|
+
raise ValueError(f"Invalid resource URI: {uri}")
|
|
508
|
+
|
|
509
|
+
session_id = uri.replace("lobster://sessions/", "")
|
|
510
|
+
session_file = self.sessions_dir / f"{session_id}.jsonl"
|
|
511
|
+
|
|
512
|
+
if not session_file.exists():
|
|
513
|
+
raise FileNotFoundError(f"Session not found: {session_id}")
|
|
514
|
+
|
|
515
|
+
with open(session_file, 'r', encoding='utf-8') as f:
|
|
516
|
+
content = f.read()
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
"contents": [
|
|
520
|
+
{
|
|
521
|
+
"uri": uri,
|
|
522
|
+
"mimeType": "application/jsonl",
|
|
523
|
+
"text": content
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
def _estimate_tokens(self, messages: List[Dict]) -> int:
|
|
529
|
+
"""估算 Token 数"""
|
|
530
|
+
total_chars = 0
|
|
531
|
+
for msg in messages:
|
|
532
|
+
content = msg.get("content", "")
|
|
533
|
+
if isinstance(content, str):
|
|
534
|
+
total_chars += len(content)
|
|
535
|
+
elif isinstance(content, list):
|
|
536
|
+
for part in content:
|
|
537
|
+
if isinstance(part, dict) and "text" in part:
|
|
538
|
+
total_chars += len(part["text"])
|
|
539
|
+
|
|
540
|
+
# 简单估算:3 字符 = 1 token
|
|
541
|
+
return total_chars // 3
|
|
542
|
+
|
|
543
|
+
def _score_message(self, msg: Dict) -> float:
|
|
544
|
+
"""评分消息重要性"""
|
|
545
|
+
content = msg.get("content", "")
|
|
546
|
+
if isinstance(content, list):
|
|
547
|
+
content = " ".join(p.get("text", "") for p in content if isinstance(p, dict))
|
|
548
|
+
|
|
549
|
+
content_lower = content.lower()
|
|
550
|
+
score = 0.0
|
|
551
|
+
|
|
552
|
+
# 关键词评分
|
|
553
|
+
patterns = {
|
|
554
|
+
"decision": ["决定", "方案", "选择", "决定采用", "decide", "choose"],
|
|
555
|
+
"error": ["错误", "失败", "异常", "error", "fail", "exception"],
|
|
556
|
+
"config": ["配置", "设置", "更新", "config", "setting", "update"],
|
|
557
|
+
"preference": ["偏好", "喜欢", "希望", "prefer", "like", "want"],
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
for category, keywords in patterns.items():
|
|
561
|
+
for keyword in keywords:
|
|
562
|
+
if keyword in content_lower:
|
|
563
|
+
score += self.config["weights"].get(category, 0.1)
|
|
564
|
+
|
|
565
|
+
return score
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def emit(obj: Dict[str, Any]) -> None:
|
|
569
|
+
"""发送 JSON 响应到 stdout"""
|
|
570
|
+
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
|
571
|
+
sys.stdout.flush()
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
async def main():
|
|
575
|
+
"""主入口"""
|
|
576
|
+
import argparse
|
|
577
|
+
import time
|
|
578
|
+
|
|
579
|
+
parser = argparse.ArgumentParser(description="LobsterPress MCP Server")
|
|
580
|
+
parser.add_argument("--sessions-dir", help="会话目录")
|
|
581
|
+
parser.add_argument("--db", dest="db_path", help="LobsterPress 数据库路径")
|
|
582
|
+
parser.add_argument("--provider", dest="llm_provider", help="LLM 提供商")
|
|
583
|
+
parser.add_argument("--model", dest="llm_model", help="LLM 模型名称")
|
|
584
|
+
parser.add_argument("--test", action="store_true", help="测试模式")
|
|
585
|
+
|
|
586
|
+
args = parser.parse_args()
|
|
587
|
+
|
|
588
|
+
server = LobsterPressMCPServer(
|
|
589
|
+
sessions_dir=args.sessions_dir,
|
|
590
|
+
db_path=args.db_path,
|
|
591
|
+
llm_provider=args.llm_provider,
|
|
592
|
+
llm_model=args.llm_model
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if args.test:
|
|
596
|
+
# 测试模式
|
|
597
|
+
print("=== LobsterPress MCP Server 测试 ===")
|
|
598
|
+
print("\n可用工具:")
|
|
599
|
+
for tool in server.tools:
|
|
600
|
+
print(f" - {tool.name}: {tool.description}")
|
|
601
|
+
|
|
602
|
+
# 测试 list_sessions
|
|
603
|
+
result = await server._call_tool("list_sessions", {"min_tokens": 1000})
|
|
604
|
+
print(f"\n找到 {result['total']} 个可压缩会话")
|
|
605
|
+
for session in result["sessions"][:3]:
|
|
606
|
+
print(f" - {session['session_id']}: {session['estimated_tokens']} tokens")
|
|
607
|
+
|
|
608
|
+
print("\n✅ MCP Server 正常工作")
|
|
609
|
+
else:
|
|
610
|
+
# Phase 1 (Issue #115): 发送 ready handshake
|
|
611
|
+
emit({"type": "lobster-press/ready", "ts": time.time()})
|
|
612
|
+
|
|
613
|
+
# MCP 协议模式(读取 stdin)
|
|
614
|
+
for line in sys.stdin:
|
|
615
|
+
line = line.strip()
|
|
616
|
+
if not line:
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
request_id = None
|
|
620
|
+
try:
|
|
621
|
+
req = json.loads(line)
|
|
622
|
+
request_id = req.get("requestId") or req.get("id")
|
|
623
|
+
method = req.get("method")
|
|
624
|
+
|
|
625
|
+
if method == "tools/call":
|
|
626
|
+
params = req.get("params", {})
|
|
627
|
+
tool_name = params.get("name")
|
|
628
|
+
arguments = params.get("arguments", {})
|
|
629
|
+
result = await server._call_tool(tool_name, arguments)
|
|
630
|
+
emit({
|
|
631
|
+
"requestId": request_id,
|
|
632
|
+
"status": "ok",
|
|
633
|
+
"result": result,
|
|
634
|
+
})
|
|
635
|
+
else:
|
|
636
|
+
# 其他方法使用原有处理逻辑
|
|
637
|
+
response = await server.handle_request(req)
|
|
638
|
+
emit({
|
|
639
|
+
"requestId": request_id,
|
|
640
|
+
"status": "ok",
|
|
641
|
+
"result": response,
|
|
642
|
+
})
|
|
643
|
+
except json.JSONDecodeError as e:
|
|
644
|
+
emit({
|
|
645
|
+
"requestId": request_id,
|
|
646
|
+
"status": "error",
|
|
647
|
+
"error": f"Invalid JSON: {e}",
|
|
648
|
+
})
|
|
649
|
+
except Exception as e:
|
|
650
|
+
emit({
|
|
651
|
+
"requestId": request_id,
|
|
652
|
+
"status": "error",
|
|
653
|
+
"error": str(e),
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
if __name__ == "__main__":
|
|
658
|
+
asyncio.run(main())
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonicbotman/lobster-press",
|
|
3
|
-
"version": "3.2.
|
|
4
|
-
"description": "Cognitive Memory System for AI Agents
|
|
3
|
+
"version": "3.2.6",
|
|
4
|
+
"description": "Cognitive Memory System for AI Agents \u2014 OpenClaw Plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/",
|
|
10
|
+
"mcp_server/",
|
|
10
11
|
"openclaw.plugin.json",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
@@ -29,7 +30,6 @@
|
|
|
29
30
|
"openclaw": ">=2026.3.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
|
-
"@sinclair/typebox": "^0.34.48",
|
|
33
33
|
"openclaw": "^2026.3.0",
|
|
34
34
|
"typescript": "^5.4.0"
|
|
35
35
|
},
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/SonicBotMan/lobster-press#readme",
|
|
44
44
|
"openclaw": {
|
|
45
|
-
"extensions": [
|
|
45
|
+
"extensions": [
|
|
46
|
+
"./dist/index.js"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@sinclair/typebox": "^0.34.48"
|
|
46
51
|
}
|
|
47
52
|
}
|