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.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: ima-python-sdk
3
+ Version: 0.1.0
4
+ Summary: 非官方的 IMA OpenAPI Python SDK,提供知识库和笔记管理功能
5
+ Author: elxy
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/elxy/ima-python-sdk
8
+ Project-URL: Repository, https://github.com/elxy/ima-python-sdk
9
+ Project-URL: Issues, https://github.com/elxy/ima-python-sdk/issues
10
+ Keywords: ima,knowledge-base,notes,sdk
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Operating System :: OS Independent
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # ima-python-sdk
27
+
28
+ 非官方的 IMA OpenAPI Python SDK,提供知识库和笔记管理功能。
29
+
30
+ > 本项目参考 [ima-skills](https://clawhub.ai/iampennyli/ima-skills) 中的 API 文档和脚本实现,与 IMA 官方无关。
31
+
32
+ ## 安装
33
+
34
+ ```bash
35
+ pip install ima-python-sdk
36
+ ```
37
+
38
+ ## 配置
39
+
40
+ SDK 需要 `client_id` 和 `api_key` 两个凭证,按以下优先级自动发现:
41
+
42
+ 1. **构造参数** — `ImaClient(client_id="...", api_key="...")`
43
+ 2. **环境变量** — `IMA_OPENAPI_CLIENTID` / `IMA_OPENAPI_APIKEY`
44
+ 3. **配置文件** — `~/.config/ima/client_id` / `~/.config/ima/api_key`
45
+
46
+ ## CLI 使用
47
+
48
+ `--kb_id` 可通过环境变量 `IMA_KB_ID` 设置,避免每次手动指定。
49
+
50
+ ```bash
51
+ # 知识库
52
+ ima-sdk kb addable
53
+ ima-sdk kb upload report.pdf --kb_id xxx # 上传到根目录
54
+ ima-sdk kb upload report.pdf 论文/机器学习 --kb_id xxx # 上传到指定目录
55
+ ima-sdk kb import-url https://example.com --kb_id xxx
56
+
57
+ # 笔记
58
+ ima-sdk note search "会议记录"
59
+ ima-sdk note create "# 标题"
60
+ ima-sdk note read <doc_id> --raw
61
+ ```
62
+
63
+ ## SDK 使用
64
+
65
+ ```python
66
+ from ima_sdk import ImaClient, KnowledgeBaseManager, NotesManager
67
+
68
+ client = ImaClient() # 自动读取环境变量或配置文件
69
+ kb = KnowledgeBaseManager(client)
70
+ notes = NotesManager(client)
71
+
72
+ # 知识库操作
73
+ kb.upload_file("/path/to/report.pdf", knowledge_base_id="xxx")
74
+ kb.import_urls(["https://example.com"], knowledge_base_id="xxx")
75
+ results = kb.search_knowledge("关键词", knowledge_base_id="xxx")
76
+
77
+ # 笔记操作
78
+ doc_id = notes.create("# 标题\n\n正文内容")
79
+ docs, total, is_end = notes.search("会议记录")
80
+ content = notes.get_content(doc_id)
81
+ ```
82
+
83
+ ## 特性
84
+
85
+ - **零第三方依赖** — 仅使用 Python 标准库(urllib, hmac, hashlib, dataclasses)
86
+ - **凭证自动发现** — 构造参数 > 环境变量 > 配置文件,三级优先
87
+ - **自动翻页** — 列表接口支持 `fetch_all=True`
88
+ - **完整上传流程** — `upload_file()` 一个方法完成预检、重名检查、创建媒体、COS 上传、添加知识
89
+ - **UTF-8 安全** — 笔记写入前自动校验编码
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,15 @@
1
+ ima_python_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=Mi2ivYnXBdi7QDrfLPnxgyujUcGuHNEGBV-1b4kYVhc,1061
2
+ ima_sdk/__init__.py,sha256=oJTlVRs08wdQkye-IQzZaJesNA1Uf3XOm7Fp6RiJu-k,1956
3
+ ima_sdk/cli.py,sha256=m3yOfH8bFf9ZQJwZ1772fmHFXHVu1_Ie1pCUgpynDfs,15708
4
+ ima_sdk/client.py,sha256=Tx21aJSWDRkzpOkxHPKTFS_L1MF-511k8MYHH-Hnnu4,9717
5
+ ima_sdk/cos_uploader.py,sha256=BEINVD4N8PbE_nHe0XocLtQFPLt-46T-qm1l9RJD0UI,4813
6
+ ima_sdk/file_checker.py,sha256=LWO-yeOIFFGXvvqqvF2CWDyzp9n9nsxlHtK5KDsw0yI,8266
7
+ ima_sdk/knowledge_base.py,sha256=8n-cr-J2lQaDm8Lqmww_CrUsYh9Imw_vLBbOsSho7ek,20965
8
+ ima_sdk/logger.py,sha256=FEYNSM96G-b2wRJkGHcnoOArUD22Fm4wrstfuYYrIbY,2395
9
+ ima_sdk/notes.py,sha256=GoRyn1qXs3AL9zu6oSRCuYXepVxNtVfOCG7hH8wfNfM,11323
10
+ ima_sdk/types.py,sha256=oHYCsEo1YK_6fTxZC4lpPC4U_XZh6W5QuSTVy_XK3is,11346
11
+ ima_python_sdk-0.1.0.dist-info/METADATA,sha256=GwP77JyDpPlRwTCda0e7Rk8RopSwoczXYfLIESNa8Aw,3117
12
+ ima_python_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ ima_python_sdk-0.1.0.dist-info/entry_points.txt,sha256=dugqsz4PVgamfgxXz9TdDoZEfgQMkNKAkNRKRCgHy6s,45
14
+ ima_python_sdk-0.1.0.dist-info/top_level.txt,sha256=sZundEb6Z_upgSYPSF_XyJLnHM9k45r9lgzdfLfoPLw,8
15
+ ima_python_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ima-sdk = ima_sdk.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 elxy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ima_sdk
ima_sdk/__init__.py ADDED
@@ -0,0 +1,90 @@
1
+ """IMA Python SDK — 知识库和笔记操作。
2
+
3
+ 用法::
4
+
5
+ from ima_sdk import ImaClient, KnowledgeBaseManager, NotesManager
6
+
7
+ client = ImaClient()
8
+ kb = KnowledgeBaseManager(client)
9
+ notes = NotesManager(client)
10
+
11
+ # 搜索知识库
12
+ results = kb.search_knowledge_base("我的资料")
13
+
14
+ # 上传文件到知识库
15
+ kb.upload_file("/path/to/report.pdf", knowledge_base_id="xxx")
16
+
17
+ # 创建笔记
18
+ doc_id = notes.create("# 标题\\n\\n正文内容")
19
+
20
+ # 搜索笔记
21
+ docs, total, is_end = notes.search("会议记录")
22
+ """
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ from .client import ImaApiError, ImaClient
27
+ from .cos_uploader import cos_upload
28
+ from .file_checker import check_file
29
+ from .knowledge_base import KnowledgeBaseManager
30
+ from .notes import NotesManager
31
+ from .types import (
32
+ AddableKnowledgeBaseInfo,
33
+ CheckRepeatedNameResult,
34
+ ContentFormat,
35
+ CosCredential,
36
+ CreateMediaResult,
37
+ DocBasic,
38
+ FileCheckResult,
39
+ FileInfo,
40
+ FolderType,
41
+ ImportURLResult,
42
+ KBFolderInfo,
43
+ KnowledgeBaseInfo,
44
+ KnowledgeInfo,
45
+ KnowledgeListResult,
46
+ MediaType,
47
+ NoteBookInfo,
48
+ NoteFolderItem,
49
+ SearchedDoc,
50
+ SearchedKnowledgeBaseInfo,
51
+ SearchedKnowledgeInfo,
52
+ SearchType,
53
+ SortType,
54
+ )
55
+
56
+ __all__ = [
57
+ # 核心
58
+ "ImaClient",
59
+ "ImaApiError",
60
+ # 管理器
61
+ "KnowledgeBaseManager",
62
+ "NotesManager",
63
+ # 工具函数
64
+ "check_file",
65
+ "cos_upload",
66
+ # 类型
67
+ "KnowledgeBaseInfo",
68
+ "KnowledgeInfo",
69
+ "KBFolderInfo",
70
+ "AddableKnowledgeBaseInfo",
71
+ "SearchedKnowledgeBaseInfo",
72
+ "SearchedKnowledgeInfo",
73
+ "ImportURLResult",
74
+ "CosCredential",
75
+ "FileInfo",
76
+ "CreateMediaResult",
77
+ "CheckRepeatedNameResult",
78
+ "KnowledgeListResult",
79
+ "FileCheckResult",
80
+ "DocBasic",
81
+ "NoteFolderItem",
82
+ "NoteBookInfo",
83
+ "SearchedDoc",
84
+ # 枚举
85
+ "MediaType",
86
+ "ContentFormat",
87
+ "SearchType",
88
+ "SortType",
89
+ "FolderType",
90
+ ]
ima_sdk/cli.py ADDED
@@ -0,0 +1,447 @@
1
+ """IMA SDK 命令行工具。
2
+
3
+ 使用 argparse 子命令模式提供知识库和笔记的命令行操作。
4
+
5
+ 用法::
6
+
7
+ # 知识库操作
8
+ ima-sdk kb search-base <query>
9
+ ima-sdk kb info <kb_id>
10
+ ima-sdk kb list <kb_id>
11
+ ima-sdk kb search <query> --kb_id <kb_id>
12
+ ima-sdk kb upload <file_path> [dest_dir] --kb_id <kb_id>
13
+ ima-sdk kb upload <file_path> [dest_dir] # kb_id 从 IMA_KB_ID 环境变量读取
14
+ ima-sdk kb import-url <url> --kb_id <kb_id>
15
+ ima-sdk kb addable
16
+
17
+ # 笔记操作
18
+ ima-sdk note search <query>
19
+ ima-sdk note folders
20
+ ima-sdk note list [--folder_id <id>]
21
+ ima-sdk note create <content_or_file>
22
+ ima-sdk note append <doc_id> <content_or_file>
23
+ ima-sdk note read <doc_id>
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import os
31
+ import sys
32
+ from typing import Any
33
+
34
+
35
+ def _json_output(obj: Any) -> str:
36
+ """将对象序列化为可读的 JSON 字符串。"""
37
+ if hasattr(obj, "__dataclass_fields__"):
38
+ from dataclasses import asdict
39
+
40
+ return json.dumps(asdict(obj), ensure_ascii=False, indent=2)
41
+ if isinstance(obj, list):
42
+ from dataclasses import asdict
43
+
44
+ items = []
45
+ for item in obj:
46
+ if hasattr(item, "__dataclass_fields__"):
47
+ items.append(asdict(item))
48
+ elif isinstance(item, dict):
49
+ items.append(item)
50
+ else:
51
+ items.append(str(item))
52
+ return json.dumps(items, ensure_ascii=False, indent=2)
53
+ if isinstance(obj, dict):
54
+ from dataclasses import asdict
55
+
56
+ result = {}
57
+ for k, v in obj.items():
58
+ if hasattr(v, "__dataclass_fields__"):
59
+ result[k] = asdict(v)
60
+ else:
61
+ result[k] = v
62
+ return json.dumps(result, ensure_ascii=False, indent=2)
63
+ return json.dumps(obj, ensure_ascii=False, indent=2)
64
+
65
+
66
+ def _read_content(value: str) -> str:
67
+ """读取内容:如果 value 是存在的文件路径则读取文件,否则作为直接内容。"""
68
+ if os.path.isfile(value):
69
+ with open(value, "r", encoding="utf-8") as f:
70
+ return f.read()
71
+ return value
72
+
73
+
74
+ def _get_client(*, curl_mode: bool = False):
75
+ """创建 ImaClient 实例。"""
76
+ from .client import ImaClient
77
+
78
+ return ImaClient(curl_mode=curl_mode)
79
+
80
+
81
+ def _require_kb_id(args: argparse.Namespace) -> str:
82
+ """校验 kb_id 存在,缺失时报错退出。"""
83
+ if not args.kb_id:
84
+ print(
85
+ "错误: 必须通过 --kb_id 或环境变量 IMA_KB_ID 指定知识库 ID",
86
+ file=sys.stderr,
87
+ )
88
+ sys.exit(1)
89
+ return args.kb_id
90
+
91
+
92
+ # ── 知识库子命令 ──────────────────────────────────────────────────────────────
93
+
94
+
95
+ def _kb_search_base(args: argparse.Namespace) -> None:
96
+ from .knowledge_base import KnowledgeBaseManager
97
+
98
+ kb = KnowledgeBaseManager(_get_client(curl_mode=args.curl))
99
+ results = kb.search_knowledge_base(args.query, fetch_all=args.all)
100
+ print(_json_output(results))
101
+
102
+
103
+ def _kb_info(args: argparse.Namespace) -> None:
104
+ from .knowledge_base import KnowledgeBaseManager
105
+
106
+ kb = KnowledgeBaseManager(_get_client(curl_mode=args.curl))
107
+ results = kb.get_knowledge_base(args.ids)
108
+ print(_json_output(results))
109
+
110
+
111
+ def _kb_list(args: argparse.Namespace) -> None:
112
+ from .knowledge_base import KnowledgeBaseManager
113
+
114
+ _require_kb_id(args)
115
+ kb = KnowledgeBaseManager(_get_client(curl_mode=args.curl))
116
+ if args.all:
117
+ knowledge, folders = kb.get_all_knowledge(
118
+ args.kb_id, folder_id=args.folder_id
119
+ )
120
+ print(
121
+ _json_output({"knowledge": knowledge, "folders": folders})
122
+ )
123
+ else:
124
+ result = kb.get_knowledge_list(
125
+ args.kb_id, folder_id=args.folder_id, limit=args.limit
126
+ )
127
+ print(_json_output(result))
128
+
129
+
130
+ def _kb_search(args: argparse.Namespace) -> None:
131
+ from .knowledge_base import KnowledgeBaseManager
132
+
133
+ _require_kb_id(args)
134
+ kb = KnowledgeBaseManager(_get_client(curl_mode=args.curl))
135
+ results = kb.search_knowledge(
136
+ args.query, args.kb_id, fetch_all=args.all
137
+ )
138
+ print(_json_output(results))
139
+
140
+
141
+ def _kb_upload(args: argparse.Namespace) -> None:
142
+ from .knowledge_base import KnowledgeBaseManager
143
+
144
+ _require_kb_id(args)
145
+ kb = KnowledgeBaseManager(_get_client(curl_mode=args.curl))
146
+
147
+ # dest_dir 和 --folder_id 互斥,dest_dir 需要逐级解析
148
+ folder_id = args.folder_id
149
+ if args.dest_dir:
150
+ if folder_id:
151
+ print("错误: --folder_id 和 dest_dir 不能同时指定", file=sys.stderr)
152
+ sys.exit(1)
153
+ folder_id = kb.resolve_folder_path(args.kb_id, args.dest_dir)
154
+
155
+ if args.dry_run:
156
+ result = kb.dry_run_upload(
157
+ args.file_path,
158
+ args.kb_id,
159
+ folder_id=folder_id,
160
+ )
161
+ print(_json_output(result))
162
+ else:
163
+ media_id = kb.upload_file(
164
+ args.file_path,
165
+ args.kb_id,
166
+ folder_id=folder_id,
167
+ skip_duplicate_check=args.skip_dup,
168
+ )
169
+ print(json.dumps({"media_id": media_id}, ensure_ascii=False))
170
+
171
+
172
+ def _kb_import_url(args: argparse.Namespace) -> None:
173
+ from .knowledge_base import KnowledgeBaseManager
174
+
175
+ _require_kb_id(args)
176
+ kb = KnowledgeBaseManager(_get_client(curl_mode=args.curl))
177
+ results = kb.import_urls(
178
+ args.urls, args.kb_id, folder_id=args.folder_id
179
+ )
180
+ print(_json_output(results))
181
+
182
+
183
+ def _kb_addable(args: argparse.Namespace) -> None:
184
+ from .knowledge_base import KnowledgeBaseManager
185
+
186
+ kb = KnowledgeBaseManager(_get_client(curl_mode=args.curl))
187
+ results = kb.get_addable_list(fetch_all=args.all)
188
+ print(_json_output(results))
189
+
190
+
191
+ # ── 笔记子命令 ────────────────────────────────────────────────────────────────
192
+
193
+
194
+ def _note_search(args: argparse.Namespace) -> None:
195
+ from .notes import NotesManager
196
+
197
+ notes = NotesManager(_get_client(curl_mode=args.curl))
198
+ if args.all:
199
+ results = notes.search_all(
200
+ args.query, search_type=args.search_type, sort_type=args.sort_type
201
+ )
202
+ print(_json_output(results))
203
+ else:
204
+ docs, total, is_end = notes.search(
205
+ args.query,
206
+ search_type=args.search_type,
207
+ sort_type=args.sort_type,
208
+ start=args.start,
209
+ end=args.end,
210
+ )
211
+ print(
212
+ _json_output(
213
+ {"docs": docs, "total_hit_num": total, "is_end": is_end}
214
+ )
215
+ )
216
+
217
+
218
+ def _note_folders(args: argparse.Namespace) -> None:
219
+ from .notes import NotesManager
220
+
221
+ notes = NotesManager(_get_client(curl_mode=args.curl))
222
+ if args.all:
223
+ results = notes.list_all_folders()
224
+ else:
225
+ results = notes.list_folders(limit=args.limit)
226
+ print(_json_output(results))
227
+
228
+
229
+ def _note_list(args: argparse.Namespace) -> None:
230
+ from .notes import NotesManager
231
+
232
+ notes = NotesManager(_get_client(curl_mode=args.curl))
233
+ if args.all:
234
+ results = notes.list_all_notes(folder_id=args.folder_id)
235
+ print(_json_output(results))
236
+ else:
237
+ note_list, is_end, next_cursor = notes.list_notes(
238
+ folder_id=args.folder_id, limit=args.limit
239
+ )
240
+ print(
241
+ _json_output(
242
+ {
243
+ "notes": note_list,
244
+ "is_end": is_end,
245
+ "next_cursor": next_cursor,
246
+ }
247
+ )
248
+ )
249
+
250
+
251
+ def _note_create(args: argparse.Namespace) -> None:
252
+ from .notes import NotesManager
253
+
254
+ notes = NotesManager(_get_client(curl_mode=args.curl))
255
+ content = _read_content(args.content)
256
+ doc_id = notes.create(content, folder_id=args.folder_id)
257
+ print(json.dumps({"doc_id": doc_id}, ensure_ascii=False))
258
+
259
+
260
+ def _note_append(args: argparse.Namespace) -> None:
261
+ from .notes import NotesManager
262
+
263
+ notes = NotesManager(_get_client(curl_mode=args.curl))
264
+ content = _read_content(args.content)
265
+ doc_id = notes.append(args.doc_id, content)
266
+ print(json.dumps({"doc_id": doc_id}, ensure_ascii=False))
267
+
268
+
269
+ def _note_read(args: argparse.Namespace) -> None:
270
+ from .notes import NotesManager
271
+
272
+ notes = NotesManager(_get_client(curl_mode=args.curl))
273
+ content = notes.get_content(args.doc_id, target_format=args.format)
274
+ if args.raw:
275
+ print(content)
276
+ else:
277
+ print(json.dumps({"content": content}, ensure_ascii=False, indent=2))
278
+
279
+
280
+ # ── 主解析器 ──────────────────────────────────────────────────────────────────
281
+
282
+
283
+ def build_parser() -> argparse.ArgumentParser:
284
+ """构建命令行解析器。"""
285
+ parser = argparse.ArgumentParser(
286
+ prog="ima-sdk",
287
+ description="IMA 知识库和笔记命令行工具",
288
+ )
289
+ parser.add_argument(
290
+ "-v", "--verbose",
291
+ action="store_true",
292
+ help="启用详细日志输出(输出到 stderr)",
293
+ )
294
+ parser.add_argument(
295
+ "--curl",
296
+ action="store_true",
297
+ help="输出等价的 curl 命令到 stderr,方便调试",
298
+ )
299
+ subparsers = parser.add_subparsers(dest="module", help="功能模块")
300
+
301
+ # ── kb 模块 ───────────────────────────────────────────────────────────
302
+
303
+ kb_parser = subparsers.add_parser("kb", help="知识库操作")
304
+ kb_sub = kb_parser.add_subparsers(dest="command", help="知识库命令")
305
+
306
+ # kb search-base
307
+ p = kb_sub.add_parser("search-base", help="搜索知识库列表")
308
+ p.add_argument("query", help="搜索关键词")
309
+ p.add_argument("--all", action="store_true", help="获取全部结果(自动翻页)")
310
+ p.set_defaults(func=_kb_search_base)
311
+
312
+ # kb info
313
+ p = kb_sub.add_parser("info", help="获取知识库信息")
314
+ p.add_argument("ids", nargs="+", help="知识库 ID(支持多个)")
315
+ p.set_defaults(func=_kb_info)
316
+
317
+ # kb list
318
+ p = kb_sub.add_parser("list", help="浏览知识库内容")
319
+ p.add_argument("kb_id", nargs="?", default=os.environ.get("IMA_KB_ID"), help="知识库 ID(默认读取环境变量 IMA_KB_ID)")
320
+ p.add_argument("--folder_id", help="文件夹 ID")
321
+ p.add_argument("--limit", type=int, default=50, help="每页数量(默认 50)")
322
+ p.add_argument("--all", action="store_true", help="获取全部(自动翻页)")
323
+ p.set_defaults(func=_kb_list)
324
+
325
+ # kb search
326
+ p = kb_sub.add_parser("search", help="搜索知识库内容")
327
+ p.add_argument("query", help="搜索关键词")
328
+ p.add_argument("--kb_id", default=os.environ.get("IMA_KB_ID"), help="知识库 ID(默认读取环境变量 IMA_KB_ID)")
329
+ p.add_argument("--all", action="store_true", help="获取全部结果")
330
+ p.set_defaults(func=_kb_search)
331
+
332
+ # kb upload
333
+ p = kb_sub.add_parser("upload", help="上传文件到知识库")
334
+ p.add_argument("file_path", help="文件路径")
335
+ p.add_argument("dest_dir", nargs="?", default="", help="目标目录路径,如 '论文/机器学习'(默认根目录)")
336
+ p.add_argument("--kb_id", default=os.environ.get("IMA_KB_ID"), help="知识库 ID(默认读取环境变量 IMA_KB_ID)")
337
+ p.add_argument("--folder_id", help="文件夹 ID(与 dest_dir 互斥)")
338
+ p.add_argument("--skip_dup", action="store_true", help="跳过重名检查")
339
+ p.add_argument("--dry_run", action="store_true", help="只做预检和重名检查,不实际上传")
340
+ p.set_defaults(func=_kb_upload)
341
+
342
+ # kb import-url
343
+ p = kb_sub.add_parser("import-url", help="导入网页到知识库")
344
+ p.add_argument("urls", nargs="+", help="URL 列表(1-10 个)")
345
+ p.add_argument("--kb_id", default=os.environ.get("IMA_KB_ID"), help="知识库 ID(默认读取环境变量 IMA_KB_ID)")
346
+ p.add_argument("--folder_id", help="文件夹 ID")
347
+ p.set_defaults(func=_kb_import_url)
348
+
349
+ # kb addable
350
+ p = kb_sub.add_parser("addable", help="列出可添加的知识库")
351
+ p.add_argument("--all", action="store_true", help="获取全部结果")
352
+ p.set_defaults(func=_kb_addable)
353
+
354
+ # ── note 模块 ─────────────────────────────────────────────────────────
355
+
356
+ note_parser = subparsers.add_parser("note", help="笔记操作")
357
+ note_sub = note_parser.add_subparsers(dest="command", help="笔记命令")
358
+
359
+ # note search
360
+ p = note_sub.add_parser("search", help="搜索笔记")
361
+ p.add_argument("query", help="搜索关键词")
362
+ p.add_argument(
363
+ "--search_type", type=int, default=0, help="检索方式: 0=标题, 1=正文"
364
+ )
365
+ p.add_argument(
366
+ "--sort_type",
367
+ type=int,
368
+ default=0,
369
+ help="排序: 0=更新时间, 1=创建时间, 2=标题, 3=大小",
370
+ )
371
+ p.add_argument("--start", type=int, default=0, help="翻页起始位置")
372
+ p.add_argument("--end", type=int, default=20, help="翻页结束位置")
373
+ p.add_argument("--all", action="store_true", help="获取全部结果")
374
+ p.set_defaults(func=_note_search)
375
+
376
+ # note folders
377
+ p = note_sub.add_parser("folders", help="列出笔记本")
378
+ p.add_argument("--limit", type=int, default=50, help="数量限制")
379
+ p.add_argument("--all", action="store_true", help="获取全部")
380
+ p.set_defaults(func=_note_folders)
381
+
382
+ # note list
383
+ p = note_sub.add_parser("list", help="列出笔记")
384
+ p.add_argument("--folder_id", help="笔记本 ID")
385
+ p.add_argument("--limit", type=int, default=50, help="数量限制")
386
+ p.add_argument("--all", action="store_true", help="获取全部")
387
+ p.set_defaults(func=_note_list)
388
+
389
+ # note create
390
+ p = note_sub.add_parser("create", help="创建笔记")
391
+ p.add_argument("content", help="笔记内容(Markdown)或文件路径")
392
+ p.add_argument("--folder_id", help="笔记本 ID")
393
+ p.set_defaults(func=_note_create)
394
+
395
+ # note append
396
+ p = note_sub.add_parser("append", help="追加内容到笔记")
397
+ p.add_argument("doc_id", help="目标笔记 ID")
398
+ p.add_argument("content", help="追加内容(Markdown)或文件路径")
399
+ p.set_defaults(func=_note_append)
400
+
401
+ # note read
402
+ p = note_sub.add_parser("read", help="读取笔记内容")
403
+ p.add_argument("doc_id", help="笔记 ID")
404
+ p.add_argument(
405
+ "--format", type=int, default=0, help="目标格式: 0=纯文本, 2=JSON"
406
+ )
407
+ p.add_argument("--raw", action="store_true", help="直接输出内容(不包裹 JSON)")
408
+ p.set_defaults(func=_note_read)
409
+
410
+ return parser
411
+
412
+
413
+ def main() -> None:
414
+ """命令行入口函数。"""
415
+ parser = build_parser()
416
+ args = parser.parse_args()
417
+
418
+ # 处理 verbose 开关
419
+ if args.verbose:
420
+ from . import logger
421
+ logger.enable()
422
+
423
+ if not args.module:
424
+ parser.print_help()
425
+ sys.exit(1)
426
+
427
+ if not args.command:
428
+ # 打印子模块帮助
429
+ if args.module == "kb":
430
+ parser.parse_args(["kb", "-h"])
431
+ elif args.module == "note":
432
+ parser.parse_args(["note", "-h"])
433
+ sys.exit(1)
434
+
435
+ if not hasattr(args, "func"):
436
+ parser.print_help()
437
+ sys.exit(1)
438
+
439
+ try:
440
+ args.func(args)
441
+ except Exception as e:
442
+ print(f"错误: {e}", file=sys.stderr)
443
+ sys.exit(1)
444
+
445
+
446
+ if __name__ == "__main__":
447
+ main()