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/__init__.py
ADDED
read/cli.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""CLI 主入口
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
- 统一的 JSON 输出
|
|
5
|
+
- 统一的错误处理
|
|
6
|
+
- Typer 应用配置
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import traceback
|
|
14
|
+
import typer
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from dong import json_output, ValidationError, NotFoundError, ConflictError
|
|
18
|
+
from read import __version__
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="read",
|
|
22
|
+
help="读咚咚 (Read) - 个人知识数据层的命令行接口",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
add_completion=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def output(data: Any, success: bool = True) -> None:
|
|
29
|
+
"""输出 JSON 格式"""
|
|
30
|
+
result: dict[str, Any] = {"success": success}
|
|
31
|
+
if success:
|
|
32
|
+
result["data"] = data
|
|
33
|
+
else:
|
|
34
|
+
result["error"] = data
|
|
35
|
+
typer.echo(json.dumps(result, ensure_ascii=False, indent=2))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def handle_error(e: Exception) -> None:
|
|
39
|
+
"""处理异常并输出结构化错误"""
|
|
40
|
+
error_info: dict[str, str] = {
|
|
41
|
+
"code": type(e).__name__,
|
|
42
|
+
"message": str(e),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if "--debug" in sys.argv:
|
|
46
|
+
error_info["traceback"] = traceback.format_exc()
|
|
47
|
+
|
|
48
|
+
output(error_info, success=False)
|
|
49
|
+
raise typer.Exit(code=1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.callback()
|
|
53
|
+
def version_callback(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
version: bool = typer.Option(False, "--version", "-v", help="显示版本号"),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""版本号回调"""
|
|
58
|
+
if version:
|
|
59
|
+
typer.echo(f"read {__version__}")
|
|
60
|
+
raise typer.Exit()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
@json_output
|
|
65
|
+
def init():
|
|
66
|
+
"""初始化数据库"""
|
|
67
|
+
from read.commands.init import cmd_init
|
|
68
|
+
result = cmd_init()
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
@json_output
|
|
74
|
+
def add(
|
|
75
|
+
content: str = typer.Argument(None, help="摘录内容"),
|
|
76
|
+
url: str = typer.Option(None, "--url", "-u", help="链接"),
|
|
77
|
+
source: str = typer.Option(None, "--source", "-s", help="来源备注"),
|
|
78
|
+
type: str = typer.Option("quote", "--type", "-t", help="数据类型(quote/article/code)"),
|
|
79
|
+
):
|
|
80
|
+
"""添加摘录或链接"""
|
|
81
|
+
from read.commands.add import cmd_add
|
|
82
|
+
result = cmd_add(content=content, url=url, source=source, item_type=type)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
@json_output
|
|
88
|
+
def ls(
|
|
89
|
+
limit: int = typer.Option(20, "--limit", "-l", help="返回数量"),
|
|
90
|
+
offset: int = typer.Option(0, "--offset", "-o", help="偏移量"),
|
|
91
|
+
type: str = typer.Option(None, "--type", "-t", help="筛选类型(content/link)"),
|
|
92
|
+
order: str = typer.Option("desc", "--order", help="排序方向(desc/asc)"),
|
|
93
|
+
):
|
|
94
|
+
"""列出所有摘录"""
|
|
95
|
+
from read.commands.ls import cmd_ls
|
|
96
|
+
result = cmd_ls(limit=limit, offset=offset, item_type=type, order=order)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command("get")
|
|
101
|
+
@json_output
|
|
102
|
+
def get_item(
|
|
103
|
+
item_id: int = typer.Argument(..., help="摘录 ID"),
|
|
104
|
+
field: str = typer.Option(None, "--field", "-f", help="只返回指定字段"),
|
|
105
|
+
):
|
|
106
|
+
"""获取单条摘录"""
|
|
107
|
+
from read.commands.get import cmd_get
|
|
108
|
+
result = cmd_get(item_id=item_id, field=field)
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.command()
|
|
113
|
+
@json_output
|
|
114
|
+
def delete(
|
|
115
|
+
item_ids: list[int] = typer.Argument(..., help="摘录 ID(支持多个)"),
|
|
116
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制删除,不确认"),
|
|
117
|
+
):
|
|
118
|
+
"""删除摘录"""
|
|
119
|
+
from read.commands.delete import cmd_delete
|
|
120
|
+
result = cmd_delete(item_ids=item_ids, force=force)
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
@json_output
|
|
126
|
+
def search(
|
|
127
|
+
query: str = typer.Argument(..., help="搜索关键词"),
|
|
128
|
+
field: str = typer.Option(None, "--field", "-f", help="搜索字段(content/url/source)"),
|
|
129
|
+
limit: int = typer.Option(20, "--limit", "-l", help="返回数量"),
|
|
130
|
+
):
|
|
131
|
+
"""搜索摘录"""
|
|
132
|
+
from read.commands.search import cmd_search
|
|
133
|
+
result = cmd_search(query=query, field=field, limit=limit)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
app()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""CLI 命令实现"""
|
|
2
|
+
|
|
3
|
+
from read.commands.init import cmd_init
|
|
4
|
+
from read.commands.add import cmd_add
|
|
5
|
+
from read.commands.ls import cmd_ls
|
|
6
|
+
from read.commands.get import cmd_get
|
|
7
|
+
from read.commands.delete import cmd_delete
|
|
8
|
+
from read.commands.search import cmd_search
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"cmd_init",
|
|
12
|
+
"cmd_add",
|
|
13
|
+
"cmd_ls",
|
|
14
|
+
"cmd_get",
|
|
15
|
+
"cmd_delete",
|
|
16
|
+
"cmd_search",
|
|
17
|
+
]
|
read/commands/add.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""add 命令 - 添加摘录"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from read.core.client import Client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_add(
|
|
9
|
+
content: Optional[str],
|
|
10
|
+
url: Optional[str],
|
|
11
|
+
source: Optional[str],
|
|
12
|
+
item_type: str = "quote",
|
|
13
|
+
) -> dict:
|
|
14
|
+
"""添加摘录
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
content: 摘录内容
|
|
18
|
+
url: 链接
|
|
19
|
+
source: 来源备注
|
|
20
|
+
item_type: 数据类型
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
添加结果
|
|
24
|
+
"""
|
|
25
|
+
client = Client()
|
|
26
|
+
item = client.add(
|
|
27
|
+
content=content,
|
|
28
|
+
url=url,
|
|
29
|
+
source=source,
|
|
30
|
+
item_type=item_type,
|
|
31
|
+
)
|
|
32
|
+
return item.to_dict()
|
read/commands/delete.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""delete 命令 - 删除摘录"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from read.core.client import Client
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cmd_delete(item_ids: List[int], force: bool = False) -> dict:
|
|
10
|
+
"""删除摘录
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
item_ids: 摘录 ID 列表
|
|
14
|
+
force: 是否强制删除
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
删除结果
|
|
18
|
+
"""
|
|
19
|
+
client = Client()
|
|
20
|
+
|
|
21
|
+
if not force and len(item_ids) == 1:
|
|
22
|
+
# 单个删除时确认
|
|
23
|
+
item = client.get_optional(item_ids[0])
|
|
24
|
+
if item:
|
|
25
|
+
confirm = typer.confirm(
|
|
26
|
+
f"确定要删除这条摘录吗?\n\n {item.display_text[:50]}"
|
|
27
|
+
)
|
|
28
|
+
if not confirm:
|
|
29
|
+
return {"deleted": False, "message": "取消删除"}
|
|
30
|
+
|
|
31
|
+
deleted = []
|
|
32
|
+
not_found = []
|
|
33
|
+
|
|
34
|
+
for item_id in item_ids:
|
|
35
|
+
if client.delete(item_id):
|
|
36
|
+
deleted.append(item_id)
|
|
37
|
+
else:
|
|
38
|
+
not_found.append(item_id)
|
|
39
|
+
|
|
40
|
+
result: dict = {
|
|
41
|
+
"deleted": deleted,
|
|
42
|
+
"deleted_count": len(deleted),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if not_found:
|
|
46
|
+
result["not_found"] = not_found
|
|
47
|
+
result["not_found_count"] = len(not_found)
|
|
48
|
+
|
|
49
|
+
return result
|
read/commands/get.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""get 命令 - 获取单条摘录"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from read.core.client import Client
|
|
6
|
+
from read.core.client import NotFoundError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cmd_get(item_id: int, field: Optional[str] = None) -> Any:
|
|
10
|
+
"""获取单条摘录
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
item_id: 摘录 ID
|
|
14
|
+
field: 只返回指定字段
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
摘录数据或指定字段值
|
|
18
|
+
"""
|
|
19
|
+
client = Client()
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
item = client.get(item_id)
|
|
23
|
+
except NotFoundError:
|
|
24
|
+
return {
|
|
25
|
+
"error": "not_found",
|
|
26
|
+
"message": f"Item {item_id} not found",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if field:
|
|
30
|
+
return item.to_dict().get(field)
|
|
31
|
+
|
|
32
|
+
return item.to_dict()
|
read/commands/init.py
ADDED
read/commands/ls.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""ls 命令 - 列出所有摘录"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from read.core.client import Client
|
|
6
|
+
from read.db.utils import count_total
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cmd_ls(
|
|
10
|
+
limit: int = 20,
|
|
11
|
+
offset: int = 0,
|
|
12
|
+
item_type: Optional[str] = None,
|
|
13
|
+
order: str = "desc",
|
|
14
|
+
) -> dict:
|
|
15
|
+
"""列出摘录
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
limit: 返回数量
|
|
19
|
+
offset: 偏移量
|
|
20
|
+
item_type: 筛选类型
|
|
21
|
+
order: 排序方向
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
列表结果
|
|
25
|
+
"""
|
|
26
|
+
client = Client()
|
|
27
|
+
|
|
28
|
+
# 类型映射
|
|
29
|
+
type_map = {
|
|
30
|
+
"content": "quote",
|
|
31
|
+
"link": "article",
|
|
32
|
+
"code": "code",
|
|
33
|
+
}
|
|
34
|
+
db_type = type_map.get(item_type, item_type) if item_type else None
|
|
35
|
+
|
|
36
|
+
items = client.list(
|
|
37
|
+
limit=limit,
|
|
38
|
+
offset=offset,
|
|
39
|
+
item_type=db_type,
|
|
40
|
+
order=order,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
"total": count_total(),
|
|
45
|
+
"count": len(items),
|
|
46
|
+
"items": [item.to_dict() for item in items],
|
|
47
|
+
}
|
read/commands/search.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""search 命令 - 搜索摘录"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from read.core.client import Client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cmd_search(
|
|
9
|
+
query: str,
|
|
10
|
+
field: Optional[str] = None,
|
|
11
|
+
limit: int = 20,
|
|
12
|
+
) -> dict:
|
|
13
|
+
"""搜索摘录
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
query: 搜索关键词
|
|
17
|
+
field: 搜索字段
|
|
18
|
+
limit: 返回数量
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
搜索结果
|
|
22
|
+
"""
|
|
23
|
+
client = Client()
|
|
24
|
+
items = client.search(query=query, field=field, limit=limit)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"query": query,
|
|
28
|
+
"field": field,
|
|
29
|
+
"count": len(items),
|
|
30
|
+
"items": [item.to_dict() for item in items],
|
|
31
|
+
}
|
read/config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""配置管理模块
|
|
2
|
+
|
|
3
|
+
继承 dong.config.Config,管理 read-cli 的用户配置。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dong.config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ReadConfig(Config):
|
|
10
|
+
"""读咚咚配置类"""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def get_name(cls) -> str:
|
|
14
|
+
return "read"
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def get_defaults(cls) -> dict:
|
|
18
|
+
return {
|
|
19
|
+
"default_status": "reading",
|
|
20
|
+
"default_limit": 20,
|
|
21
|
+
"statuses": ["reading", "completed", "abandoned"],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# 便捷函数
|
|
26
|
+
def get_config() -> dict:
|
|
27
|
+
return ReadConfig.load()
|
|
28
|
+
|
|
29
|
+
def get_default_status() -> str:
|
|
30
|
+
return ReadConfig.get("default_status", "reading")
|
|
31
|
+
|
|
32
|
+
def get_default_limit() -> int:
|
|
33
|
+
return ReadConfig.get("default_limit", 20)
|
|
34
|
+
|
|
35
|
+
def get_statuses() -> list:
|
|
36
|
+
return ReadConfig.get("statuses", ["reading", "completed", "abandoned"])
|
read/const.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""常量定义"""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
VERSION = "0.1.0"
|
|
6
|
+
DB_NAME = "read.db"
|
|
7
|
+
# 数据目录 - 统一放在 ~/.dong/ 下
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
DB_DIR = Path.home() / ".dong" / "read"
|
|
10
|
+
|
|
11
|
+
# 默认配置
|
|
12
|
+
DEFAULT_LIMIT = 20
|
|
13
|
+
DEFAULT_TYPE = "quote"
|
|
14
|
+
|
|
15
|
+
# 数据类型
|
|
16
|
+
TYPE_QUOTE = "quote" # 文字摘录
|
|
17
|
+
TYPE_ARTICLE = "article" # 文章链接
|
|
18
|
+
TYPE_CODE = "code" # 代码片段
|
|
19
|
+
|
|
20
|
+
ALL_TYPES = [TYPE_QUOTE, TYPE_ARTICLE, TYPE_CODE]
|
|
21
|
+
|
|
22
|
+
# 时间格式
|
|
23
|
+
ISO_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_timestamp() -> str:
|
|
27
|
+
"""获取当前时间戳(ISO 8601 格式)"""
|
|
28
|
+
return datetime.datetime.now().strftime(ISO_FORMAT)
|
read/core/__init__.py
ADDED
read/core/client.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Python SDK / Core Library
|
|
2
|
+
|
|
3
|
+
这是读咚咚的核心资产。所有客户端(CLI、MCP、HTTP API)都通过此层访问数据。
|
|
4
|
+
|
|
5
|
+
设计原则:
|
|
6
|
+
1. 独立性 - 不依赖任何客户端
|
|
7
|
+
2. 完整性 - 包含所有数据操作逻辑
|
|
8
|
+
3. 稳定性 - API 考虑向后兼容
|
|
9
|
+
4. 可测试性 - 可独立单元测试
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from read.core.models import Item
|
|
18
|
+
from read.db.utils import (
|
|
19
|
+
add_item as db_add_item,
|
|
20
|
+
count_total as db_count_total,
|
|
21
|
+
delete_item as db_delete_item,
|
|
22
|
+
get_item as db_get_item,
|
|
23
|
+
list_items as db_list_items,
|
|
24
|
+
search_items as db_search_items,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Client:
|
|
29
|
+
"""读咚咚客户端
|
|
30
|
+
|
|
31
|
+
这是 Core Library 的主要入口点,提供给 Agent 和开发者使用。
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> from read import Client
|
|
35
|
+
>>> client = Client()
|
|
36
|
+
>>> item = client.add("开始,就是最好的时机")
|
|
37
|
+
>>> print(item.id)
|
|
38
|
+
1
|
|
39
|
+
>>> items = client.list(limit=10)
|
|
40
|
+
>>> results = client.search("AI")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, db_path: Optional[Path] = None):
|
|
44
|
+
"""初始化客户端
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
db_path: 数据库路径,None 则使用默认路径 (~/.read/read.db)
|
|
48
|
+
"""
|
|
49
|
+
self._db_path = db_path
|
|
50
|
+
# 注:v0.1 使用默认连接,db_path 参数为 v0.2 多数据库支持预留
|
|
51
|
+
|
|
52
|
+
def add(
|
|
53
|
+
self,
|
|
54
|
+
content: Optional[str] = None,
|
|
55
|
+
url: Optional[str] = None,
|
|
56
|
+
source: Optional[str] = None,
|
|
57
|
+
item_type: str = "quote",
|
|
58
|
+
metadata: Optional[str] = None,
|
|
59
|
+
) -> Item:
|
|
60
|
+
"""添加摘录
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
content: 摘录内容
|
|
64
|
+
url: 链接
|
|
65
|
+
source: 来源备注
|
|
66
|
+
item_type: 数据类型(quote/article/code)
|
|
67
|
+
metadata: JSON 扩展字段
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
创建的 Item 对象
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: content 和 url 都为空
|
|
74
|
+
"""
|
|
75
|
+
item_id = db_add_item(
|
|
76
|
+
content=content,
|
|
77
|
+
url=url,
|
|
78
|
+
source=source,
|
|
79
|
+
item_type=item_type,
|
|
80
|
+
metadata=metadata,
|
|
81
|
+
)
|
|
82
|
+
return self.get(item_id)
|
|
83
|
+
|
|
84
|
+
def list(
|
|
85
|
+
self,
|
|
86
|
+
limit: int = 20,
|
|
87
|
+
offset: int = 0,
|
|
88
|
+
item_type: Optional[str] = None,
|
|
89
|
+
order: str = "desc",
|
|
90
|
+
) -> list[Item]:
|
|
91
|
+
"""列出摘录
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
limit: 返回数量限制
|
|
95
|
+
offset: 偏移量
|
|
96
|
+
item_type: 筛选类型
|
|
97
|
+
order: 排序方向(desc/asc)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Item 列表
|
|
101
|
+
"""
|
|
102
|
+
rows = db_list_items(
|
|
103
|
+
limit=limit,
|
|
104
|
+
offset=offset,
|
|
105
|
+
item_type=item_type,
|
|
106
|
+
order=order,
|
|
107
|
+
)
|
|
108
|
+
return [Item.from_dict(row) for row in rows]
|
|
109
|
+
|
|
110
|
+
def get(self, item_id: int) -> Item:
|
|
111
|
+
"""获取单条摘录
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
item_id: 摘录 ID
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Item 对象
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
NotFoundError: 摘录不存在
|
|
121
|
+
"""
|
|
122
|
+
row = db_get_item(item_id)
|
|
123
|
+
if row is None:
|
|
124
|
+
raise NotFoundError(f"Item {item_id} not found")
|
|
125
|
+
return Item.from_dict(row)
|
|
126
|
+
|
|
127
|
+
def get_optional(self, item_id: int) -> Optional[Item]:
|
|
128
|
+
"""获取单条摘录(不存在返回 None)
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
item_id: 摘录 ID
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Item 对象或 None
|
|
135
|
+
"""
|
|
136
|
+
row = db_get_item(item_id)
|
|
137
|
+
return Item.from_dict(row) if row else None
|
|
138
|
+
|
|
139
|
+
def delete(self, item_id: int) -> bool:
|
|
140
|
+
"""删除摘录
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
item_id: 摘录 ID
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
是否删除成功
|
|
147
|
+
"""
|
|
148
|
+
return db_delete_item(item_id)
|
|
149
|
+
|
|
150
|
+
def search(
|
|
151
|
+
self,
|
|
152
|
+
query: str,
|
|
153
|
+
field: Optional[str] = None,
|
|
154
|
+
limit: int = 20,
|
|
155
|
+
) -> list[Item]:
|
|
156
|
+
"""搜索摘录
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
query: 搜索关键词
|
|
160
|
+
field: 搜索字段(content/url/source),None 表示全部
|
|
161
|
+
limit: 返回数量限制
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
匹配的 Item 列表
|
|
165
|
+
"""
|
|
166
|
+
rows = db_search_items(query=query, field=field, limit=limit)
|
|
167
|
+
return [Item.from_dict(row) for row in rows]
|
|
168
|
+
|
|
169
|
+
def count(self) -> int:
|
|
170
|
+
"""获取摘录总数
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
总数
|
|
174
|
+
"""
|
|
175
|
+
return db_count_total()
|
|
176
|
+
|
|
177
|
+
# 链式调用支持(为 Agent 提供更友好的 API)
|
|
178
|
+
def search_query(self, query: str) -> "QueryBuilder":
|
|
179
|
+
"""开始搜索查询
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
>>> client.search_query("AI").limit(5).execute()
|
|
183
|
+
"""
|
|
184
|
+
return QueryBuilder(self, query)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class QueryBuilder:
|
|
188
|
+
"""查询构建器(链式调用)"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, client: Client, query: str):
|
|
191
|
+
self._client = client
|
|
192
|
+
self._query = query
|
|
193
|
+
self._field: Optional[str] = None
|
|
194
|
+
self._limit = 20
|
|
195
|
+
|
|
196
|
+
def by_field(self, field: str) -> "QueryBuilder":
|
|
197
|
+
"""指定搜索字段"""
|
|
198
|
+
self._field = field
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
def limit(self, limit: int) -> "QueryBuilder":
|
|
202
|
+
"""设置返回数量"""
|
|
203
|
+
self._limit = limit
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def execute(self) -> list[Item]:
|
|
207
|
+
"""执行查询"""
|
|
208
|
+
return self._client.search(
|
|
209
|
+
query=self._query,
|
|
210
|
+
field=self._field,
|
|
211
|
+
limit=self._limit,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class NotFoundError(Exception):
|
|
216
|
+
"""摘录不存在异常"""
|
|
217
|
+
pass
|