read-cli 0.2.0__py3-none-any.whl
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.
- read/__init__.py +11 -0
- read/cli.py +138 -0
- read/commands/__init__.py +17 -0
- read/commands/add.py +32 -0
- read/commands/delete.py +49 -0
- read/commands/get.py +32 -0
- read/commands/init.py +12 -0
- read/commands/ls.py +47 -0
- read/commands/search.py +31 -0
- read/config.py +36 -0
- read/const.py +28 -0
- read/core/__init__.py +9 -0
- read/core/client.py +217 -0
- read/core/models.py +80 -0
- read/db/__init__.py +12 -0
- read/db/connection.py +37 -0
- read/db/schema.py +60 -0
- read/db/utils.py +272 -0
- read/mcp/__init__.py +7 -0
- read_cli-0.2.0.dist-info/METADATA +218 -0
- read_cli-0.2.0.dist-info/RECORD +24 -0
- read_cli-0.2.0.dist-info/WHEEL +4 -0
- read_cli-0.2.0.dist-info/entry_points.txt +2 -0
- read_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
read/core/models.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""数据模型
|
|
2
|
+
|
|
3
|
+
Core Library 的数据结构定义。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Item:
|
|
14
|
+
"""摘录数据模型
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
id: 主键 ID
|
|
18
|
+
content: 摘录内容
|
|
19
|
+
url: 链接
|
|
20
|
+
source: 来源备注
|
|
21
|
+
type: 数据类型(quote/article/code)
|
|
22
|
+
metadata: JSON 扩展字段
|
|
23
|
+
created_at: 创建时间(ISO 8601)
|
|
24
|
+
updated_at: 更新时间(ISO 8601)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
id: int
|
|
28
|
+
content: Optional[str] = None
|
|
29
|
+
url: Optional[str] = None
|
|
30
|
+
source: Optional[str] = None
|
|
31
|
+
type: str = "quote"
|
|
32
|
+
metadata: Optional[str] = None
|
|
33
|
+
created_at: Optional[str] = None
|
|
34
|
+
updated_at: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_quote(self) -> bool:
|
|
38
|
+
"""是否为文字摘录"""
|
|
39
|
+
return self.type == "quote" and self.content is not None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def is_link(self) -> bool:
|
|
43
|
+
"""是否为链接"""
|
|
44
|
+
return self.url is not None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def display_text(self) -> str:
|
|
48
|
+
"""显示文本(优先 content,其次 url)"""
|
|
49
|
+
if self.content:
|
|
50
|
+
return self.content
|
|
51
|
+
if self.url:
|
|
52
|
+
return self.url
|
|
53
|
+
return "(空)"
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict:
|
|
56
|
+
"""转换为字典"""
|
|
57
|
+
return {
|
|
58
|
+
"id": self.id,
|
|
59
|
+
"content": self.content,
|
|
60
|
+
"url": self.url,
|
|
61
|
+
"source": self.source,
|
|
62
|
+
"type": self.type,
|
|
63
|
+
"metadata": self.metadata,
|
|
64
|
+
"created_at": self.created_at,
|
|
65
|
+
"updated_at": self.updated_at,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, data: dict) -> "Item":
|
|
70
|
+
"""从字典创建"""
|
|
71
|
+
return cls(
|
|
72
|
+
id=data["id"],
|
|
73
|
+
content=data.get("content"),
|
|
74
|
+
url=data.get("url"),
|
|
75
|
+
source=data.get("source"),
|
|
76
|
+
type=data.get("type", "quote"),
|
|
77
|
+
metadata=data.get("metadata"),
|
|
78
|
+
created_at=data.get("created_at"),
|
|
79
|
+
updated_at=data.get("updated_at"),
|
|
80
|
+
)
|
read/db/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""数据库层"""
|
|
2
|
+
|
|
3
|
+
from .connection import ReadDatabase, get_connection, close_connection, get_cursor, get_db_path
|
|
4
|
+
from .schema import ReadSchemaManager, SCHEMA_VERSION, get_schema_version, set_schema_version, is_initialized, init_database
|
|
5
|
+
from .utils import to_cents, from_cents
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ReadDatabase", "ReadSchemaManager",
|
|
9
|
+
"get_connection", "close_connection", "get_cursor", "get_db_path",
|
|
10
|
+
"SCHEMA_VERSION", "get_schema_version", "set_schema_version", "is_initialized", "init_database",
|
|
11
|
+
"to_cents", "from_cents",
|
|
12
|
+
]
|
read/db/connection.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""数据库连接管理模块
|
|
2
|
+
|
|
3
|
+
继承 dong.db.Database,提供 read-cli 专用数据库访问。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
from dong.db import Database as DongDatabase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReadDatabase(DongDatabase):
|
|
14
|
+
"""读咚咚数据库类 - 继承自 dong.db.Database
|
|
15
|
+
|
|
16
|
+
数据库路径: ~/.read/read.db
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_name(cls) -> str:
|
|
21
|
+
return "read"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 兼容性函数
|
|
25
|
+
def get_connection(db_path=None):
|
|
26
|
+
return ReadDatabase.get_connection()
|
|
27
|
+
|
|
28
|
+
def close_connection():
|
|
29
|
+
ReadDatabase.close_connection()
|
|
30
|
+
|
|
31
|
+
@contextmanager
|
|
32
|
+
def get_cursor() -> Iterator[sqlite3.Cursor]:
|
|
33
|
+
with ReadDatabase.get_cursor() as cur:
|
|
34
|
+
yield cur
|
|
35
|
+
|
|
36
|
+
def get_db_path():
|
|
37
|
+
return ReadDatabase.get_db_path()
|
read/db/schema.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""数据库 Schema 定义和版本管理
|
|
2
|
+
|
|
3
|
+
继承 dong.db.SchemaManager,管理 read-cli 的数据库 schema。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dong.db import SchemaManager
|
|
7
|
+
from .connection import ReadDatabase
|
|
8
|
+
|
|
9
|
+
SCHEMA_VERSION = "1.0.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReadSchemaManager(SchemaManager):
|
|
13
|
+
"""读咚咚 Schema 管理器"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__(
|
|
17
|
+
db_class=ReadDatabase,
|
|
18
|
+
current_version=SCHEMA_VERSION
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def init_schema(self) -> None:
|
|
22
|
+
self._create_articles_table()
|
|
23
|
+
self._create_indexes()
|
|
24
|
+
|
|
25
|
+
def _create_articles_table(self) -> None:
|
|
26
|
+
with ReadDatabase.get_cursor() as cur:
|
|
27
|
+
cur.execute("""
|
|
28
|
+
CREATE TABLE IF NOT EXISTS articles (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
title TEXT NOT NULL,
|
|
31
|
+
author TEXT,
|
|
32
|
+
url TEXT,
|
|
33
|
+
notes TEXT,
|
|
34
|
+
status TEXT DEFAULT 'reading',
|
|
35
|
+
rating INTEGER,
|
|
36
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
38
|
+
)
|
|
39
|
+
""")
|
|
40
|
+
|
|
41
|
+
def _create_indexes(self) -> None:
|
|
42
|
+
with ReadDatabase.get_cursor() as cur:
|
|
43
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_articles_status ON articles(status)")
|
|
44
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_articles_author ON articles(author)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# 兼容性函数
|
|
48
|
+
def get_schema_version() -> str | None:
|
|
49
|
+
return ReadSchemaManager().get_stored_version()
|
|
50
|
+
|
|
51
|
+
def set_schema_version(version: str) -> None:
|
|
52
|
+
ReadDatabase.set_meta(ReadSchemaManager.VERSION_KEY, version)
|
|
53
|
+
|
|
54
|
+
def is_initialized() -> bool:
|
|
55
|
+
return ReadSchemaManager().is_initialized()
|
|
56
|
+
|
|
57
|
+
def init_database() -> None:
|
|
58
|
+
schema = ReadSchemaManager()
|
|
59
|
+
if not schema.is_initialized():
|
|
60
|
+
schema.initialize()
|
read/db/utils.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""CRUD 操作封装
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
- 封装所有数据库操作
|
|
5
|
+
- 提供类型友好的接口
|
|
6
|
+
- 处理业务逻辑验证
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from read.const import DEFAULT_LIMIT, DEFAULT_TYPE, get_timestamp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_item(
|
|
17
|
+
content: Optional[str] = None,
|
|
18
|
+
url: Optional[str] = None,
|
|
19
|
+
source: Optional[str] = None,
|
|
20
|
+
item_type: str = DEFAULT_TYPE,
|
|
21
|
+
metadata: Optional[str] = None,
|
|
22
|
+
) -> int:
|
|
23
|
+
"""添加摘录,返回新 ID
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
content: 摘录内容
|
|
27
|
+
url: 链接
|
|
28
|
+
source: 来源备注
|
|
29
|
+
item_type: 数据类型
|
|
30
|
+
metadata: JSON 扩展字段
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
新插入记录的 ID
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: content 和 url 都为空
|
|
37
|
+
"""
|
|
38
|
+
from read.db.connection import get_cursor
|
|
39
|
+
|
|
40
|
+
# 验证
|
|
41
|
+
if not content and not url:
|
|
42
|
+
raise ValueError("content 和 url 至少需要一个不为空")
|
|
43
|
+
|
|
44
|
+
now = get_timestamp()
|
|
45
|
+
|
|
46
|
+
with get_cursor() as cursor:
|
|
47
|
+
cursor.execute(
|
|
48
|
+
"""
|
|
49
|
+
INSERT INTO items (content, url, source, type, metadata, created_at, updated_at)
|
|
50
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
51
|
+
""",
|
|
52
|
+
(content, url, source, item_type, metadata, now, now),
|
|
53
|
+
)
|
|
54
|
+
return cursor.lastrowid
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def list_items(
|
|
58
|
+
limit: int = DEFAULT_LIMIT,
|
|
59
|
+
offset: int = 0,
|
|
60
|
+
item_type: Optional[str] = None,
|
|
61
|
+
order: str = "desc",
|
|
62
|
+
) -> list[dict]:
|
|
63
|
+
"""列出摘录
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
limit: 返回数量限制
|
|
67
|
+
offset: 偏移量
|
|
68
|
+
item_type: 筛选类型
|
|
69
|
+
order: 排序方向(desc/asc)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
摘录列表
|
|
73
|
+
"""
|
|
74
|
+
from read.db.connection import get_cursor
|
|
75
|
+
|
|
76
|
+
order_sql = "DESC" if order.lower() == "desc" else "ASC"
|
|
77
|
+
|
|
78
|
+
with get_cursor() as cursor:
|
|
79
|
+
if item_type:
|
|
80
|
+
cursor.execute(
|
|
81
|
+
f"""
|
|
82
|
+
SELECT id, content, url, source, type, metadata, created_at, updated_at
|
|
83
|
+
FROM items
|
|
84
|
+
WHERE type = ?
|
|
85
|
+
ORDER BY created_at {order_sql}
|
|
86
|
+
LIMIT ? OFFSET ?
|
|
87
|
+
""",
|
|
88
|
+
(item_type, limit, offset),
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
cursor.execute(
|
|
92
|
+
f"""
|
|
93
|
+
SELECT id, content, url, source, type, metadata, created_at, updated_at
|
|
94
|
+
FROM items
|
|
95
|
+
ORDER BY created_at {order_sql}
|
|
96
|
+
LIMIT ? OFFSET ?
|
|
97
|
+
""",
|
|
98
|
+
(limit, offset),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
rows = cursor.fetchall()
|
|
102
|
+
return [dict(row) for row in rows]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_item(item_id: int) -> Optional[dict]:
|
|
106
|
+
"""获取单条摘录
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
item_id: 摘录 ID
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
摘录数据,不存在返回 None
|
|
113
|
+
"""
|
|
114
|
+
from read.db.connection import get_cursor
|
|
115
|
+
|
|
116
|
+
with get_cursor() as cursor:
|
|
117
|
+
cursor.execute(
|
|
118
|
+
"""
|
|
119
|
+
SELECT id, content, url, source, type, metadata, created_at, updated_at
|
|
120
|
+
FROM items
|
|
121
|
+
WHERE id = ?
|
|
122
|
+
""",
|
|
123
|
+
(item_id,),
|
|
124
|
+
)
|
|
125
|
+
row = cursor.fetchone()
|
|
126
|
+
return dict(row) if row else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def delete_item(item_id: int) -> bool:
|
|
130
|
+
"""删除摘录
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
item_id: 摘录 ID
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
是否删除成功
|
|
137
|
+
"""
|
|
138
|
+
from read.db.connection import get_cursor
|
|
139
|
+
|
|
140
|
+
with get_cursor() as cursor:
|
|
141
|
+
cursor.execute("DELETE FROM items WHERE id = ?", (item_id,))
|
|
142
|
+
return cursor.rowcount > 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def search_items(
|
|
146
|
+
query: str,
|
|
147
|
+
field: Optional[str] = None,
|
|
148
|
+
limit: int = DEFAULT_LIMIT,
|
|
149
|
+
) -> list[dict]:
|
|
150
|
+
"""搜索摘录
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
query: 搜索关键词
|
|
154
|
+
field: 搜索字段(content/url/source),None 表示全部
|
|
155
|
+
limit: 返回数量限制
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
匹配的摘录列表
|
|
159
|
+
"""
|
|
160
|
+
from read.db.connection import get_cursor
|
|
161
|
+
|
|
162
|
+
pattern = f"%{query}%"
|
|
163
|
+
|
|
164
|
+
with get_cursor() as cursor:
|
|
165
|
+
if field == "content":
|
|
166
|
+
cursor.execute(
|
|
167
|
+
"""
|
|
168
|
+
SELECT id, content, url, source, type, metadata, created_at, updated_at
|
|
169
|
+
FROM items
|
|
170
|
+
WHERE content LIKE ?
|
|
171
|
+
ORDER BY created_at DESC
|
|
172
|
+
LIMIT ?
|
|
173
|
+
""",
|
|
174
|
+
(pattern, limit),
|
|
175
|
+
)
|
|
176
|
+
elif field == "url":
|
|
177
|
+
cursor.execute(
|
|
178
|
+
"""
|
|
179
|
+
SELECT id, content, url, source, type, metadata, created_at, updated_at
|
|
180
|
+
FROM items
|
|
181
|
+
WHERE url LIKE ?
|
|
182
|
+
ORDER BY created_at DESC
|
|
183
|
+
LIMIT ?
|
|
184
|
+
""",
|
|
185
|
+
(pattern, limit),
|
|
186
|
+
)
|
|
187
|
+
elif field == "source":
|
|
188
|
+
cursor.execute(
|
|
189
|
+
"""
|
|
190
|
+
SELECT id, content, url, source, type, metadata, created_at, updated_at
|
|
191
|
+
FROM items
|
|
192
|
+
WHERE source LIKE ?
|
|
193
|
+
ORDER BY created_at DESC
|
|
194
|
+
LIMIT ?
|
|
195
|
+
""",
|
|
196
|
+
(pattern, limit),
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
# 全部字段
|
|
200
|
+
cursor.execute(
|
|
201
|
+
"""
|
|
202
|
+
SELECT id, content, url, source, type, metadata, created_at, updated_at
|
|
203
|
+
FROM items
|
|
204
|
+
WHERE content LIKE ? OR url LIKE ? OR source LIKE ?
|
|
205
|
+
ORDER BY created_at DESC
|
|
206
|
+
LIMIT ?
|
|
207
|
+
""",
|
|
208
|
+
(pattern, pattern, pattern, limit),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
rows = cursor.fetchall()
|
|
212
|
+
return [dict(row) for row in rows]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def count_total() -> int:
|
|
216
|
+
"""获取摘录总数"""
|
|
217
|
+
from read.db.connection import get_cursor
|
|
218
|
+
|
|
219
|
+
with get_cursor() as cursor:
|
|
220
|
+
cursor.execute("SELECT COUNT(*) as count FROM items")
|
|
221
|
+
row = cursor.fetchone()
|
|
222
|
+
return row["count"] if row else 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def update_item(
|
|
226
|
+
item_id: int,
|
|
227
|
+
content: Optional[str] = None,
|
|
228
|
+
url: Optional[str] = None,
|
|
229
|
+
source: Optional[str] = None,
|
|
230
|
+
metadata: Optional[str] = None,
|
|
231
|
+
) -> bool:
|
|
232
|
+
"""更新摘录(预留,v0.1 暂不通过 CLI 暴露)
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
item_id: 摘录 ID
|
|
236
|
+
content: 新内容
|
|
237
|
+
url: 新链接
|
|
238
|
+
source: 新来源
|
|
239
|
+
metadata: 新元数据
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
是否更新成功
|
|
243
|
+
"""
|
|
244
|
+
from read.db.connection import get_cursor
|
|
245
|
+
|
|
246
|
+
updates = []
|
|
247
|
+
params = []
|
|
248
|
+
|
|
249
|
+
if content is not None:
|
|
250
|
+
updates.append("content = ?")
|
|
251
|
+
params.append(content)
|
|
252
|
+
if url is not None:
|
|
253
|
+
updates.append("url = ?")
|
|
254
|
+
params.append(url)
|
|
255
|
+
if source is not None:
|
|
256
|
+
updates.append("source = ?")
|
|
257
|
+
params.append(source)
|
|
258
|
+
if metadata is not None:
|
|
259
|
+
updates.append("metadata = ?")
|
|
260
|
+
params.append(metadata)
|
|
261
|
+
|
|
262
|
+
if not updates:
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
updates.append("updated_at = ?")
|
|
266
|
+
params.append(get_timestamp())
|
|
267
|
+
params.append(item_id)
|
|
268
|
+
|
|
269
|
+
with get_cursor() as cursor:
|
|
270
|
+
sql = f"UPDATE items SET {', '.join(updates)} WHERE id = ?"
|
|
271
|
+
cursor.execute(sql, params)
|
|
272
|
+
return cursor.rowcount > 0
|
read/mcp/__init__.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: read-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 读咚咚 (Read) - 个人知识数据层的命令行接口
|
|
5
|
+
Author: gudong
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agent,cli,knowledge,mcp
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: dong-core>=0.3.0
|
|
17
|
+
Requires-Dist: typer>=0.12.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
22
|
+
Provides-Extra: mcp
|
|
23
|
+
Requires-Dist: mcp>=0.1.0; extra == 'mcp'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# 读咚咚 (Read) - 个人知识数据层
|
|
27
|
+
|
|
28
|
+
> 读咚咚是个人知识数据层的 Python 库,提供 CLI、SDK 和 MCP Server 等多种访问方式。
|
|
29
|
+
>
|
|
30
|
+
> 本地、私有、可编程的个人知识基础设施。
|
|
31
|
+
|
|
32
|
+
[](https://www.python.org/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](https://github.com/gudong/read)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 简介
|
|
39
|
+
|
|
40
|
+
**读咚咚** 是一个个人知识数据层,以 **Core Library** 为核心资产,支持多种客户端访问。
|
|
41
|
+
|
|
42
|
+
当你看到一句有启发的话、一篇好文章,快速存下来。CLI、浏览器插件、Agent 都可以访问这些数据。
|
|
43
|
+
|
|
44
|
+
### 核心特点
|
|
45
|
+
|
|
46
|
+
- **数据层优先** - Core Library 是核心,CLI/插件/SDK 都是客户端
|
|
47
|
+
- **本地私有** - 数据存放在 `~/.read/read.db`,不上云、不同步、不追踪
|
|
48
|
+
- **Agent 友好** - JSON 输出 + Python SDK + MCP Server(v0.2)
|
|
49
|
+
- **极简核心** - 只做收集,不做整理
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
### 方式一:从 PyPI 安装(推荐)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install read-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 方式二:从源码安装
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/gudong/read.git
|
|
65
|
+
cd read
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 初始化
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
read init
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 安装 Agent Workspace
|
|
78
|
+
|
|
79
|
+
如果你使用 OpenClaw,可以把 agent 目录复制到工作区:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 复制 agent workspace
|
|
83
|
+
mkdir -p ~/.openclaw/agents/read
|
|
84
|
+
cp -r agent/* ~/.openclaw/agents/read/
|
|
85
|
+
|
|
86
|
+
# 从模板创建 MEMORY.md
|
|
87
|
+
cp agent/MEMORY.md.template ~/.openclaw/agents/read/MEMORY.md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 快速开始
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 添加摘录
|
|
96
|
+
read add "开始,就是最好的时机"
|
|
97
|
+
|
|
98
|
+
# 收藏文章
|
|
99
|
+
read add --url "https://mp.weixin.qq.com/s/xxx"
|
|
100
|
+
|
|
101
|
+
# 列出所有
|
|
102
|
+
read ls
|
|
103
|
+
|
|
104
|
+
# 搜索
|
|
105
|
+
read search "AI"
|
|
106
|
+
|
|
107
|
+
# 删除
|
|
108
|
+
read delete 123 --force
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 项目结构
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
read/
|
|
117
|
+
├── src/read/ # CLI 源码
|
|
118
|
+
├── agent/ # Agent workspace(OpenClaw 使用)
|
|
119
|
+
│ ├── IDENTITY.md # Agent 身份
|
|
120
|
+
│ ├── SOUL.md # Agent 性格
|
|
121
|
+
│ ├── TOOLS.md # CLI 工具定义
|
|
122
|
+
│ └── MEMORY.md.template # 记忆模板
|
|
123
|
+
├── docs/ # 文档
|
|
124
|
+
├── tests/ # 测试
|
|
125
|
+
├── pyproject.toml # Python 包配置
|
|
126
|
+
└── README.md # 本文件
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Python SDK
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from read import Client
|
|
135
|
+
|
|
136
|
+
client = Client()
|
|
137
|
+
|
|
138
|
+
# 添加
|
|
139
|
+
item = client.add("开始,就是最好的时机")
|
|
140
|
+
|
|
141
|
+
# 列出
|
|
142
|
+
items = client.list(limit=10)
|
|
143
|
+
|
|
144
|
+
# 搜索
|
|
145
|
+
results = client.search("AI")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 命令参考
|
|
151
|
+
|
|
152
|
+
| 命令 | 说明 |
|
|
153
|
+
|------|------|
|
|
154
|
+
| `read init` | 初始化数据库 |
|
|
155
|
+
| `read add "内容"` | 添加摘录 |
|
|
156
|
+
| `read add --url "..."` | 收藏链接 |
|
|
157
|
+
| `read ls` | 列出所有 |
|
|
158
|
+
| `read search "关键词"` | 搜索 |
|
|
159
|
+
| `read get 123` | 获取单条 |
|
|
160
|
+
| `read delete 123` | 删除 |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 架构设计
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
┌─────────────────────────────────────────────────────────┐
|
|
168
|
+
│ 客户端层 │
|
|
169
|
+
├──────────────┬──────────────┬──────────────┬────────────┤
|
|
170
|
+
│ CLI │ Browser │ Python SDK │ MCP Server │
|
|
171
|
+
│ (read add) │ Extension │ (import) │ (Agent) │
|
|
172
|
+
└──────────────┴──────────────┴──────────────┴────────────┘
|
|
173
|
+
│
|
|
174
|
+
┌────────▼────────┐
|
|
175
|
+
│ Core Library │
|
|
176
|
+
│ (read.core.Client)│
|
|
177
|
+
└────────┬────────┘
|
|
178
|
+
│
|
|
179
|
+
┌────────▼────────┐
|
|
180
|
+
│ SQLite DB │
|
|
181
|
+
│ ~/.read/read.db │
|
|
182
|
+
└─────────────────┘
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 路线图
|
|
188
|
+
|
|
189
|
+
| 版本 | 核心资产 | 客户端 | 状态 |
|
|
190
|
+
|------|----------|--------|------|
|
|
191
|
+
| v0.1 | Core Library v0.1 | CLI | ✅ 完成 |
|
|
192
|
+
| v0.2 | Core Library v0.1 | **MCP Server** | 🚧 开发中 |
|
|
193
|
+
| v0.3 | Core Library v0.1 | Python SDK 增强 | 📋 计划中 |
|
|
194
|
+
| v0.4 | Core Library v0.1 | Browser Extension | 📋 计划中 |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 文档
|
|
199
|
+
|
|
200
|
+
- [API 参考文档](docs/API_REFERENCE.md)
|
|
201
|
+
- [架构设计](ARCHITECTURE.md)
|
|
202
|
+
- [产品理念](WHY.md)
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
[MIT](LICENSE)
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 作者
|
|
213
|
+
|
|
214
|
+
[@gudong](https://github.com/gudong)
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
**让 AI 看见你的知识。**
|