jfox-cli 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.
- jfox/__init__.py +5 -0
- jfox/__main__.py +6 -0
- jfox/bm25_index.py +388 -0
- jfox/cli.py +1895 -0
- jfox/config.py +180 -0
- jfox/embedding_backend.py +65 -0
- jfox/formatters.py +305 -0
- jfox/global_config.py +281 -0
- jfox/graph.py +331 -0
- jfox/indexer.py +366 -0
- jfox/kb_manager.py +316 -0
- jfox/models.py +144 -0
- jfox/note.py +464 -0
- jfox/performance.py +408 -0
- jfox/search_engine.py +237 -0
- jfox/template.py +301 -0
- jfox/template_cli.py +327 -0
- jfox/vector_store.py +200 -0
- jfox_cli-0.1.0.dist-info/METADATA +637 -0
- jfox_cli-0.1.0.dist-info/RECORD +22 -0
- jfox_cli-0.1.0.dist-info/WHEEL +4 -0
- jfox_cli-0.1.0.dist-info/entry_points.txt +2 -0
jfox/config.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""配置管理"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_default_kb_path() -> Path:
|
|
13
|
+
"""获取默认知识库路径(从全局配置)"""
|
|
14
|
+
try:
|
|
15
|
+
from .global_config import get_global_config_manager
|
|
16
|
+
return get_global_config_manager().get_default_kb_path()
|
|
17
|
+
except Exception:
|
|
18
|
+
return Path.home() / ".zettelkasten"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ZKConfig:
|
|
23
|
+
"""Zettelkasten 配置"""
|
|
24
|
+
# 路径
|
|
25
|
+
base_dir: Path = field(default_factory=get_default_kb_path)
|
|
26
|
+
notes_dir: Path = field(init=False)
|
|
27
|
+
zk_dir: Path = field(init=False)
|
|
28
|
+
chroma_dir: Path = field(init=False)
|
|
29
|
+
|
|
30
|
+
# NPU 配置
|
|
31
|
+
embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
|
|
32
|
+
embedding_dimension: int = 384
|
|
33
|
+
device: str = "auto" # auto / npu / gpu / cpu
|
|
34
|
+
batch_size: int = 32
|
|
35
|
+
|
|
36
|
+
# 检索配置
|
|
37
|
+
default_semantic_top: int = 5
|
|
38
|
+
default_graph_hops: int = 2
|
|
39
|
+
similarity_threshold: float = 0.7
|
|
40
|
+
|
|
41
|
+
# 同步配置
|
|
42
|
+
auto_sync: bool = True
|
|
43
|
+
sync_interval: int = 30 # 秒
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
self.notes_dir = self.base_dir / "notes"
|
|
47
|
+
self.zk_dir = self.base_dir / ".zk"
|
|
48
|
+
self.chroma_dir = self.zk_dir / "chroma_db"
|
|
49
|
+
|
|
50
|
+
def ensure_dirs(self):
|
|
51
|
+
"""确保目录存在"""
|
|
52
|
+
dirs = [
|
|
53
|
+
self.notes_dir / "fleeting",
|
|
54
|
+
self.notes_dir / "literature",
|
|
55
|
+
self.notes_dir / "permanent",
|
|
56
|
+
self.zk_dir,
|
|
57
|
+
self.chroma_dir,
|
|
58
|
+
self.zk_dir / "cache",
|
|
59
|
+
]
|
|
60
|
+
for d in dirs:
|
|
61
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
def save(self, path: Optional[Path] = None):
|
|
64
|
+
"""保存配置到文件"""
|
|
65
|
+
if path is None:
|
|
66
|
+
path = self.zk_dir / "config.yaml"
|
|
67
|
+
|
|
68
|
+
config_dict = {
|
|
69
|
+
"base_dir": str(self.base_dir),
|
|
70
|
+
"embedding_model": self.embedding_model,
|
|
71
|
+
"embedding_dimension": self.embedding_dimension,
|
|
72
|
+
"device": self.device,
|
|
73
|
+
"batch_size": self.batch_size,
|
|
74
|
+
"default_semantic_top": self.default_semantic_top,
|
|
75
|
+
"default_graph_hops": self.default_graph_hops,
|
|
76
|
+
"similarity_threshold": self.similarity_threshold,
|
|
77
|
+
"auto_sync": self.auto_sync,
|
|
78
|
+
"sync_interval": self.sync_interval,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
82
|
+
yaml.dump(config_dict, f, allow_unicode=True, sort_keys=False)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def load(cls, path: Optional[Path] = None) -> "ZKConfig":
|
|
86
|
+
"""从文件加载配置"""
|
|
87
|
+
# 首先获取默认知识库路径
|
|
88
|
+
default_path = get_default_kb_path()
|
|
89
|
+
|
|
90
|
+
if path is None:
|
|
91
|
+
path = default_path / ".zk" / "config.yaml"
|
|
92
|
+
|
|
93
|
+
if not path.exists():
|
|
94
|
+
return cls(base_dir=default_path)
|
|
95
|
+
|
|
96
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
97
|
+
config_dict = yaml.safe_load(f)
|
|
98
|
+
|
|
99
|
+
# 转换路径
|
|
100
|
+
if 'base_dir' in config_dict:
|
|
101
|
+
config_dict['base_dir'] = Path(config_dict['base_dir'])
|
|
102
|
+
else:
|
|
103
|
+
config_dict['base_dir'] = default_path
|
|
104
|
+
|
|
105
|
+
return cls(**config_dict)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def for_kb(cls, kb_path: Path) -> "ZKConfig":
|
|
109
|
+
"""为指定知识库创建配置"""
|
|
110
|
+
return cls(base_dir=kb_path)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# 全局配置实例(动态获取当前默认知识库)
|
|
114
|
+
def get_config() -> ZKConfig:
|
|
115
|
+
"""获取当前默认知识库的配置"""
|
|
116
|
+
return ZKConfig.load()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# 为了向后兼容,保留 config 变量,但使用动态获取
|
|
120
|
+
config = get_config()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@contextmanager
|
|
124
|
+
def use_kb(kb_name: Optional[str] = None):
|
|
125
|
+
"""
|
|
126
|
+
临时使用指定知识库的上下文管理器
|
|
127
|
+
|
|
128
|
+
用法:
|
|
129
|
+
with use_kb("work"):
|
|
130
|
+
# 在这个上下文中,操作都在 work 知识库上
|
|
131
|
+
note = create_note(...)
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
kb_name: 知识库名称,None 表示使用当前默认知识库
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: 知识库不存在
|
|
138
|
+
"""
|
|
139
|
+
if not kb_name:
|
|
140
|
+
yield
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
from .kb_manager import get_kb_manager
|
|
144
|
+
manager = get_kb_manager()
|
|
145
|
+
|
|
146
|
+
if not manager.config_manager.kb_exists(kb_name):
|
|
147
|
+
raise ValueError(f"Knowledge base '{kb_name}' not found")
|
|
148
|
+
|
|
149
|
+
# 保存原始配置
|
|
150
|
+
original_base_dir = config.base_dir
|
|
151
|
+
original_notes_dir = config.notes_dir
|
|
152
|
+
original_zk_dir = config.zk_dir
|
|
153
|
+
original_chroma_dir = config.chroma_dir
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# 切换到目标知识库
|
|
157
|
+
kb_path = manager.config_manager.get_kb_path(kb_name)
|
|
158
|
+
config.base_dir = kb_path
|
|
159
|
+
config.notes_dir = kb_path / "notes"
|
|
160
|
+
config.zk_dir = kb_path / ".zk"
|
|
161
|
+
config.chroma_dir = config.zk_dir / "chroma_db"
|
|
162
|
+
|
|
163
|
+
# 重置索引和搜索引擎(使用新的知识库路径)
|
|
164
|
+
try:
|
|
165
|
+
from .bm25_index import reset_bm25_index
|
|
166
|
+
from .search_engine import reset_search_engine
|
|
167
|
+
from .vector_store import reset_vector_store
|
|
168
|
+
reset_bm25_index()
|
|
169
|
+
reset_search_engine()
|
|
170
|
+
reset_vector_store()
|
|
171
|
+
except Exception:
|
|
172
|
+
pass # 忽略重置错误
|
|
173
|
+
|
|
174
|
+
yield
|
|
175
|
+
finally:
|
|
176
|
+
# 恢复原始配置
|
|
177
|
+
config.base_dir = original_base_dir
|
|
178
|
+
config.notes_dir = original_notes_dir
|
|
179
|
+
config.zk_dir = original_zk_dir
|
|
180
|
+
config.chroma_dir = original_chroma_dir
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Embedding Backend - CPU Only (Simplified)"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmbeddingBackend:
|
|
11
|
+
"""Simple CPU-based embedding backend"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, model_name: str = "sentence-transformers/all-MiniLM-L6-v2"):
|
|
14
|
+
self.model_name = model_name
|
|
15
|
+
self.model = None
|
|
16
|
+
|
|
17
|
+
def load(self):
|
|
18
|
+
"""Load the model"""
|
|
19
|
+
if self.model is not None:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from sentence_transformers import SentenceTransformer
|
|
24
|
+
self.model = SentenceTransformer(self.model_name)
|
|
25
|
+
logger.info(f"Model loaded: {self.model_name}")
|
|
26
|
+
except Exception as e:
|
|
27
|
+
logger.error(f"Failed to load model: {e}")
|
|
28
|
+
raise
|
|
29
|
+
|
|
30
|
+
def encode(self, texts: List[str], batch_size: int = 32) -> np.ndarray:
|
|
31
|
+
"""Encode texts to embeddings"""
|
|
32
|
+
if self.model is None:
|
|
33
|
+
self.load()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
return self.model.encode(
|
|
37
|
+
texts,
|
|
38
|
+
batch_size=batch_size,
|
|
39
|
+
show_progress_bar=False,
|
|
40
|
+
convert_to_numpy=True
|
|
41
|
+
)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.error(f"Encoding failed: {e}")
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
def encode_single(self, text: str) -> np.ndarray:
|
|
47
|
+
"""Encode a single text"""
|
|
48
|
+
return self.encode([text])[0]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def dimension(self) -> int:
|
|
52
|
+
"""Return embedding dimension"""
|
|
53
|
+
return 384
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Global backend instance
|
|
57
|
+
_backend: Optional[EmbeddingBackend] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_backend() -> EmbeddingBackend:
|
|
61
|
+
"""Get global embedding backend instance"""
|
|
62
|
+
global _backend
|
|
63
|
+
if _backend is None:
|
|
64
|
+
_backend = EmbeddingBackend()
|
|
65
|
+
return _backend
|
jfox/formatters.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
输出格式化器
|
|
3
|
+
|
|
4
|
+
支持多种输出格式:json, table, csv, yaml, paths, tree
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.tree import Tree
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OutputFormatter:
|
|
20
|
+
"""输出格式化器"""
|
|
21
|
+
|
|
22
|
+
SUPPORTED_FORMATS = ["json", "table", "csv", "yaml", "paths", "tree"]
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def format(cls, data: Any, format_type: str, **kwargs) -> str:
|
|
26
|
+
"""
|
|
27
|
+
根据格式类型格式化数据
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
data: 要格式化的数据
|
|
31
|
+
format_type: 格式类型
|
|
32
|
+
**kwargs: 额外参数
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
格式化后的字符串
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValueError: 不支持的格式
|
|
39
|
+
"""
|
|
40
|
+
format_type = format_type.lower()
|
|
41
|
+
|
|
42
|
+
if format_type == "json":
|
|
43
|
+
return cls.to_json(data)
|
|
44
|
+
elif format_type == "table":
|
|
45
|
+
return cls.to_table(data, **kwargs)
|
|
46
|
+
elif format_type == "csv":
|
|
47
|
+
return cls.to_csv(data, **kwargs)
|
|
48
|
+
elif format_type == "yaml":
|
|
49
|
+
return cls.to_yaml(data)
|
|
50
|
+
elif format_type == "paths":
|
|
51
|
+
return cls.to_paths(data)
|
|
52
|
+
elif format_type == "tree":
|
|
53
|
+
return cls.to_tree(data, **kwargs)
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(f"Unsupported format: {format_type}. "
|
|
56
|
+
f"Supported: {', '.join(cls.SUPPORTED_FORMATS)}")
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def to_json(data: Any) -> str:
|
|
60
|
+
"""
|
|
61
|
+
JSON 格式
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
data: 任意可序列化数据
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
JSON 字符串
|
|
68
|
+
"""
|
|
69
|
+
return json.dumps(data, ensure_ascii=False, indent=2, default=str)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def to_csv(data: List[Dict], headers: Optional[List[str]] = None) -> str:
|
|
73
|
+
"""
|
|
74
|
+
CSV 格式
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
data: 字典列表
|
|
78
|
+
headers: 自定义表头,None 则使用所有键
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
CSV 字符串
|
|
82
|
+
"""
|
|
83
|
+
if not data:
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
# 确定表头
|
|
87
|
+
if headers is None:
|
|
88
|
+
if isinstance(data[0], dict):
|
|
89
|
+
headers = list(data[0].keys())
|
|
90
|
+
else:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
# 创建 CSV
|
|
94
|
+
output = io.StringIO()
|
|
95
|
+
writer = csv.DictWriter(
|
|
96
|
+
output,
|
|
97
|
+
fieldnames=headers,
|
|
98
|
+
extrasaction='ignore',
|
|
99
|
+
quoting=csv.QUOTE_MINIMAL
|
|
100
|
+
)
|
|
101
|
+
writer.writeheader()
|
|
102
|
+
|
|
103
|
+
for row in data:
|
|
104
|
+
if isinstance(row, dict):
|
|
105
|
+
# 处理嵌套字典和列表
|
|
106
|
+
flat_row = {}
|
|
107
|
+
for key, value in row.items():
|
|
108
|
+
if isinstance(value, (list, dict)):
|
|
109
|
+
flat_row[key] = json.dumps(value, ensure_ascii=False)
|
|
110
|
+
else:
|
|
111
|
+
flat_row[key] = value
|
|
112
|
+
writer.writerow(flat_row)
|
|
113
|
+
|
|
114
|
+
return output.getvalue()
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def to_yaml(data: Any) -> str:
|
|
118
|
+
"""
|
|
119
|
+
YAML 格式
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
data: 任意可序列化数据
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
YAML 字符串
|
|
126
|
+
"""
|
|
127
|
+
return yaml.dump(
|
|
128
|
+
data,
|
|
129
|
+
allow_unicode=True,
|
|
130
|
+
sort_keys=False,
|
|
131
|
+
default_flow_style=False,
|
|
132
|
+
indent=2
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def to_paths(data: Union[List[Dict], List[Path], List[str]],
|
|
137
|
+
key: str = "filepath") -> str:
|
|
138
|
+
"""
|
|
139
|
+
路径格式 - 每行一个路径
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
data: 数据列表
|
|
143
|
+
key: 路径字段名
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
路径字符串,每行一个
|
|
147
|
+
"""
|
|
148
|
+
if not data:
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
paths = []
|
|
152
|
+
for item in data:
|
|
153
|
+
if isinstance(item, (str, Path)):
|
|
154
|
+
paths.append(str(item))
|
|
155
|
+
elif isinstance(item, dict):
|
|
156
|
+
path = item.get(key)
|
|
157
|
+
if path:
|
|
158
|
+
paths.append(str(path))
|
|
159
|
+
|
|
160
|
+
return "\n".join(paths)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def to_table(data: List[Dict],
|
|
164
|
+
columns: Optional[List[str]] = None,
|
|
165
|
+
title: Optional[str] = None) -> str:
|
|
166
|
+
"""
|
|
167
|
+
表格格式(使用 Rich)
|
|
168
|
+
|
|
169
|
+
注意:此函数返回的字符串需要通过 rich.console 渲染
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
data: 字典列表
|
|
173
|
+
columns: 显示的列,None 则显示所有
|
|
174
|
+
title: 表格标题
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
表格对象(特殊处理,需要渲染)
|
|
178
|
+
"""
|
|
179
|
+
if not data:
|
|
180
|
+
return "(No data)"
|
|
181
|
+
|
|
182
|
+
# 确定列
|
|
183
|
+
if columns is None:
|
|
184
|
+
if isinstance(data[0], dict):
|
|
185
|
+
columns = list(data[0].keys())
|
|
186
|
+
else:
|
|
187
|
+
return str(data)
|
|
188
|
+
|
|
189
|
+
# 创建表格
|
|
190
|
+
table = Table(title=title)
|
|
191
|
+
for col in columns:
|
|
192
|
+
table.add_column(str(col), overflow="fold")
|
|
193
|
+
|
|
194
|
+
# 添加行
|
|
195
|
+
for row in data:
|
|
196
|
+
if isinstance(row, dict):
|
|
197
|
+
values = []
|
|
198
|
+
for col in columns:
|
|
199
|
+
val = row.get(col, "")
|
|
200
|
+
# 处理复杂类型
|
|
201
|
+
if isinstance(val, (list, dict)):
|
|
202
|
+
val = json.dumps(val, ensure_ascii=False)[:50]
|
|
203
|
+
elif isinstance(val, Path):
|
|
204
|
+
val = str(val)
|
|
205
|
+
values.append(str(val) if val is not None else "")
|
|
206
|
+
table.add_row(*values)
|
|
207
|
+
|
|
208
|
+
# 渲染为字符串
|
|
209
|
+
console = Console(file=io.StringIO(), force_terminal=False)
|
|
210
|
+
console.print(table)
|
|
211
|
+
return console.file.getvalue()
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def to_tree(data: List[Dict],
|
|
215
|
+
root_name: str = "notes",
|
|
216
|
+
group_by: str = "type") -> str:
|
|
217
|
+
"""
|
|
218
|
+
树形格式
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
data: 笔记数据列表
|
|
222
|
+
root_name: 根节点名称
|
|
223
|
+
group_by: 分组字段
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
树形字符串
|
|
227
|
+
"""
|
|
228
|
+
if not data:
|
|
229
|
+
return f"{root_name}/"
|
|
230
|
+
|
|
231
|
+
# 按类型分组
|
|
232
|
+
groups: Dict[str, List[Dict]] = {}
|
|
233
|
+
for item in data:
|
|
234
|
+
if isinstance(item, dict):
|
|
235
|
+
group_key = str(item.get(group_by, "unknown"))
|
|
236
|
+
if group_key not in groups:
|
|
237
|
+
groups[group_key] = []
|
|
238
|
+
groups[group_key].append(item)
|
|
239
|
+
|
|
240
|
+
# 创建树
|
|
241
|
+
tree = Tree(f"[bold]{root_name}/[/bold]")
|
|
242
|
+
|
|
243
|
+
for group_name, items in sorted(groups.items()):
|
|
244
|
+
group_node = tree.add(f"[cyan]{group_name}/[/cyan]")
|
|
245
|
+
for item in items:
|
|
246
|
+
title = item.get("title", "Untitled")
|
|
247
|
+
note_id = item.get("id", "")
|
|
248
|
+
if note_id:
|
|
249
|
+
group_node.add(f"{title} ([dim]{note_id}[/dim])")
|
|
250
|
+
else:
|
|
251
|
+
group_node.add(title)
|
|
252
|
+
|
|
253
|
+
# 渲染为字符串
|
|
254
|
+
console = Console(file=io.StringIO(), force_terminal=False)
|
|
255
|
+
console.print(tree)
|
|
256
|
+
return console.file.getvalue()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def format_output(data: Any,
|
|
260
|
+
format_type: str,
|
|
261
|
+
console: Optional[Console] = None,
|
|
262
|
+
**kwargs) -> None:
|
|
263
|
+
"""
|
|
264
|
+
格式化并输出数据
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
data: 要格式化的数据
|
|
268
|
+
format_type: 格式类型
|
|
269
|
+
console: Rich console 实例,None 则创建新实例
|
|
270
|
+
**kwargs: 额外参数传递给 formatter
|
|
271
|
+
"""
|
|
272
|
+
if console is None:
|
|
273
|
+
console = Console()
|
|
274
|
+
|
|
275
|
+
if format_type == "table":
|
|
276
|
+
# table 格式需要特殊处理
|
|
277
|
+
output = OutputFormatter.to_table(data, **kwargs)
|
|
278
|
+
if output == "(No data)":
|
|
279
|
+
console.print(output)
|
|
280
|
+
else:
|
|
281
|
+
# 重新渲染表格(因为 to_table 已经返回了渲染后的字符串)
|
|
282
|
+
console.print(output)
|
|
283
|
+
elif format_type == "tree":
|
|
284
|
+
output = OutputFormatter.to_tree(data, **kwargs)
|
|
285
|
+
console.print(output)
|
|
286
|
+
else:
|
|
287
|
+
output = OutputFormatter.format(data, format_type, **kwargs)
|
|
288
|
+
console.print(output)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# 向后兼容:支持 --json 参数
|
|
292
|
+
def is_json_format(json_flag: bool, format_param: Optional[str]) -> bool:
|
|
293
|
+
"""
|
|
294
|
+
判断是否使用 JSON 格式(向后兼容)
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
json_flag: --json 标志
|
|
298
|
+
format_param: --format 参数值
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
是否使用 JSON 格式
|
|
302
|
+
"""
|
|
303
|
+
if format_param:
|
|
304
|
+
return format_param.lower() == "json"
|
|
305
|
+
return json_flag
|