ima-python-sdk 0.1.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.
ima_sdk/notes.py ADDED
@@ -0,0 +1,343 @@
1
+ """笔记管理器。
2
+
3
+ 封装 IMA 笔记相关的所有 API 操作,包括搜索、列表、创建、追加和读取。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import List, Optional, Tuple
9
+
10
+ from .client import ImaClient
11
+ from . import logger
12
+ from .types import (
13
+ ContentFormat,
14
+ DocBasic,
15
+ NoteBookInfo,
16
+ NoteFolderItem,
17
+ SearchedDoc,
18
+ SearchType,
19
+ SortType,
20
+ )
21
+
22
+ # 笔记 API 基础路径
23
+ _BASE = "openapi/note/v1"
24
+
25
+
26
+ def _ensure_utf8(text: str) -> str:
27
+ """确保文本为合法 UTF-8 编码。
28
+
29
+ 对无法编码的字符进行替换处理。
30
+ """
31
+ return text.encode("utf-8", errors="replace").decode("utf-8")
32
+
33
+
34
+ class NotesManager:
35
+ """笔记管理器。
36
+
37
+ 用法::
38
+
39
+ from ima_sdk import ImaClient, NotesManager
40
+
41
+ client = ImaClient()
42
+ notes = NotesManager(client)
43
+
44
+ # 搜索笔记
45
+ results = notes.search("会议记录")
46
+
47
+ # 创建笔记
48
+ doc_id = notes.create("# 会议纪要\\n\\n今日要点...")
49
+ """
50
+
51
+ def __init__(self, client: ImaClient):
52
+ self._client = client
53
+
54
+ # ── 搜索笔记 ──────────────────────────────────────────────────────────────
55
+
56
+ def search(
57
+ self,
58
+ query: str,
59
+ *,
60
+ search_type: int = SearchType.TITLE,
61
+ sort_type: int = SortType.MODIFY_TIME,
62
+ start: int = 0,
63
+ end: int = 20,
64
+ ) -> Tuple[List[SearchedDoc], int, bool]:
65
+ """搜索笔记。
66
+
67
+ Args:
68
+ query: 搜索关键词
69
+ search_type: 检索方式,0=标题(默认),1=正文
70
+ sort_type: 排序方式,0=更新时间, 1=创建时间, 2=标题, 3=大小
71
+ start: 翻页起始位置
72
+ end: 翻页结束位置
73
+
74
+ Returns:
75
+ (搜索结果列表, 总命中数, 是否最后一页) 的元组
76
+ """
77
+ # 构建 query_info
78
+ logger.log_step("搜索笔记", f"query={query!r} search_type={search_type} sort_type={sort_type}")
79
+ query_info = {}
80
+ if search_type == SearchType.TITLE:
81
+ query_info["title"] = query
82
+ else:
83
+ query_info["content"] = query
84
+
85
+ data = self._client.post(
86
+ f"{_BASE}/search_note_book",
87
+ {
88
+ "search_type": search_type,
89
+ "sort_type": sort_type,
90
+ "query_info": query_info,
91
+ "start": start,
92
+ "end": end,
93
+ },
94
+ )
95
+ docs = [SearchedDoc.from_dict(d) for d in data.get("docs", [])]
96
+ total = data.get("total_hit_num", 0)
97
+ is_end = data.get("is_end", True)
98
+ logger.log(f" 命中 {total} 条,本页 {len(docs)} 条,is_end={is_end}")
99
+ return docs, total, is_end
100
+
101
+ def search_all(
102
+ self,
103
+ query: str,
104
+ *,
105
+ search_type: int = SearchType.TITLE,
106
+ sort_type: int = SortType.MODIFY_TIME,
107
+ page_size: int = 20,
108
+ ) -> List[SearchedDoc]:
109
+ """搜索笔记并自动翻页获取全部结果。
110
+
111
+ Args:
112
+ query: 搜索关键词
113
+ search_type: 检索方式
114
+ sort_type: 排序方式
115
+ page_size: 每页数量
116
+
117
+ Returns:
118
+ 所有搜索结果列表
119
+ """
120
+ logger.log_step("搜索全部笔记", f"query={query!r} page_size={page_size}")
121
+ all_docs: List[SearchedDoc] = []
122
+ start = 0
123
+ while True:
124
+ docs, _, is_end = self.search(
125
+ query,
126
+ search_type=search_type,
127
+ sort_type=sort_type,
128
+ start=start,
129
+ end=start + page_size,
130
+ )
131
+ all_docs.extend(docs)
132
+ if is_end or not docs:
133
+ break
134
+ start += page_size
135
+ logger.log(f" 共获取 {len(all_docs)} 条笔记")
136
+ return all_docs
137
+
138
+ # ── 列出笔记本 ────────────────────────────────────────────────────────────
139
+
140
+ def list_folders(
141
+ self, *, limit: int = 50
142
+ ) -> List[NoteFolderItem]:
143
+ """列出所有笔记本(单页)。
144
+
145
+ Args:
146
+ limit: 数量限制
147
+
148
+ Returns:
149
+ 笔记本列表
150
+ """
151
+ logger.log_step("列出笔记本", f"limit={limit}")
152
+ data = self._client.post(
153
+ f"{_BASE}/list_note_folder_by_cursor",
154
+ {"cursor": "0", "limit": limit},
155
+ )
156
+ result = [
157
+ NoteFolderItem.from_dict(f) for f in data.get("note_book_folders", [])
158
+ ]
159
+ logger.log(f" 获取 {len(result)} 个笔记本")
160
+ return result
161
+
162
+ def list_all_folders(self, *, limit: int = 50) -> List[NoteFolderItem]:
163
+ """列出所有笔记本(自动翻页)。
164
+
165
+ Args:
166
+ limit: 每页数量
167
+
168
+ Returns:
169
+ 所有笔记本列表
170
+ """
171
+ logger.log_step("列出全部笔记本", f"limit={limit}")
172
+ all_folders: List[NoteFolderItem] = []
173
+ cursor = "0"
174
+ while True:
175
+ data = self._client.post(
176
+ f"{_BASE}/list_note_folder_by_cursor",
177
+ {"cursor": cursor, "limit": limit},
178
+ )
179
+ folders = [
180
+ NoteFolderItem.from_dict(f)
181
+ for f in data.get("note_book_folders", [])
182
+ ]
183
+ all_folders.extend(folders)
184
+ if data.get("is_end", True):
185
+ break
186
+ next_cursor = data.get("next_cursor", "")
187
+ if not next_cursor:
188
+ break
189
+ cursor = next_cursor
190
+ logger.log(f" 共获取 {len(all_folders)} 个笔记本")
191
+ return all_folders
192
+
193
+ # ── 列出笔记 ──────────────────────────────────────────────────────────────
194
+
195
+ def list_notes(
196
+ self,
197
+ *,
198
+ folder_id: Optional[str] = None,
199
+ limit: int = 50,
200
+ ) -> Tuple[List[NoteBookInfo], bool, str]:
201
+ """列出笔记本下的笔记(单页)。
202
+
203
+ Args:
204
+ folder_id: 笔记本 ID(省略则列出根目录)
205
+ limit: 数量限制
206
+
207
+ Returns:
208
+ (笔记列表, 是否最后一页, 下一页游标) 的元组
209
+ """
210
+ logger.log_step("列出笔记", f"folder_id={folder_id} limit={limit}")
211
+ body: dict = {"cursor": "", "limit": limit}
212
+ if folder_id:
213
+ body["folder_id"] = folder_id
214
+ data = self._client.post(f"{_BASE}/list_note_by_folder_id", body)
215
+ notes = [NoteBookInfo.from_dict(n) for n in data.get("note_book_list", [])]
216
+ is_end = data.get("is_end", True)
217
+ next_cursor = data.get("next_cursor", "")
218
+ logger.log(f" 获取 {len(notes)} 条笔记,is_end={is_end}")
219
+ return notes, is_end, next_cursor
220
+
221
+ def list_all_notes(
222
+ self,
223
+ *,
224
+ folder_id: Optional[str] = None,
225
+ limit: int = 50,
226
+ ) -> List[NoteBookInfo]:
227
+ """列出笔记本下的所有笔记(自动翻页)。
228
+
229
+ Args:
230
+ folder_id: 笔记本 ID
231
+ limit: 每页数量
232
+
233
+ Returns:
234
+ 所有笔记列表
235
+ """
236
+ logger.log_step("列出全部笔记", f"folder_id={folder_id}")
237
+ all_notes: List[NoteBookInfo] = []
238
+ cursor = ""
239
+ while True:
240
+ body: dict = {"cursor": cursor, "limit": limit}
241
+ if folder_id:
242
+ body["folder_id"] = folder_id
243
+ data = self._client.post(f"{_BASE}/list_note_by_folder_id", body)
244
+ notes = [
245
+ NoteBookInfo.from_dict(n) for n in data.get("note_book_list", [])
246
+ ]
247
+ all_notes.extend(notes)
248
+ if data.get("is_end", True):
249
+ break
250
+ next_cursor = data.get("next_cursor", "")
251
+ if not next_cursor:
252
+ break
253
+ cursor = next_cursor
254
+ logger.log(f" 共获取 {len(all_notes)} 条笔记")
255
+ return all_notes
256
+
257
+ # ── 创建笔记 ──────────────────────────────────────────────────────────────
258
+
259
+ def create(
260
+ self,
261
+ content: str,
262
+ *,
263
+ folder_id: Optional[str] = None,
264
+ ) -> str:
265
+ """创建新笔记(Markdown 格式)。
266
+
267
+ Args:
268
+ content: 笔记正文内容(Markdown 格式)
269
+ folder_id: 关联的笔记本 ID
270
+
271
+ Returns:
272
+ 新建笔记的 doc_id
273
+ """
274
+ content = _ensure_utf8(content)
275
+ logger.log_step("创建笔记", f"content_len={len(content)} folder_id={folder_id}")
276
+ body: dict = {
277
+ "content_format": ContentFormat.MARKDOWN,
278
+ "content": content,
279
+ }
280
+ if folder_id:
281
+ body["folder_id"] = folder_id
282
+ data = self._client.post(f"{_BASE}/import_doc", body)
283
+ doc_id = data.get("doc_id", "")
284
+ logger.log(f" 创建成功 doc_id={doc_id}")
285
+ return doc_id
286
+
287
+ # ── 追加内容到笔记 ────────────────────────────────────────────────────────
288
+
289
+ def append(self, doc_id: str, content: str) -> str:
290
+ """在笔记末尾追加内容。
291
+
292
+ 注意:追加操作不可撤销。
293
+
294
+ Args:
295
+ doc_id: 目标笔记 ID
296
+ content: 要追加的内容(Markdown 格式)
297
+
298
+ Returns:
299
+ 目标笔记的 doc_id
300
+ """
301
+ content = _ensure_utf8(content)
302
+ logger.log_step("追加笔记内容", f"doc_id={doc_id} content_len={len(content)}")
303
+ data = self._client.post(
304
+ f"{_BASE}/append_doc",
305
+ {
306
+ "doc_id": doc_id,
307
+ "content_format": ContentFormat.MARKDOWN,
308
+ "content": content,
309
+ },
310
+ )
311
+ result_id = data.get("doc_id", "")
312
+ logger.log(f" 追加成功 doc_id={result_id}")
313
+ return result_id
314
+
315
+ # ── 获取笔记内容 ──────────────────────────────────────────────────────────
316
+
317
+ def get_content(
318
+ self,
319
+ doc_id: str,
320
+ *,
321
+ target_format: int = ContentFormat.PLAINTEXT,
322
+ ) -> str:
323
+ """获取笔记内容。
324
+
325
+ Args:
326
+ doc_id: 目标笔记 ID
327
+ target_format: 目标格式,0=纯文本(推荐),2=JSON
328
+ 注意:1=Markdown 不支持
329
+
330
+ Returns:
331
+ 笔记内容文本
332
+ """
333
+ logger.log_step("读取笔记内容", f"doc_id={doc_id} format={target_format}")
334
+ data = self._client.post(
335
+ f"{_BASE}/get_doc_content",
336
+ {
337
+ "doc_id": doc_id,
338
+ "target_content_format": target_format,
339
+ },
340
+ )
341
+ content = data.get("content", "")
342
+ logger.log(f" 内容长度: {len(content)} 字符")
343
+ return content