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/template.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Template management for standardized note creation"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from jinja2 import Template, UndefinedError
|
|
10
|
+
|
|
11
|
+
from .models import NoteType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class NoteTemplate:
|
|
16
|
+
"""Template definition for creating notes"""
|
|
17
|
+
name: str
|
|
18
|
+
description: str
|
|
19
|
+
note_type: str
|
|
20
|
+
title_format: str
|
|
21
|
+
content: str
|
|
22
|
+
tags: List[str] = field(default_factory=list)
|
|
23
|
+
is_builtin: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TemplateError(Exception):
|
|
27
|
+
"""Template-related error"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TemplateNotFoundError(TemplateError):
|
|
32
|
+
"""Template not found error"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TemplateRenderError(TemplateError):
|
|
37
|
+
"""Template rendering error"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TemplateManager:
|
|
42
|
+
"""Manage note templates"""
|
|
43
|
+
|
|
44
|
+
# Built-in template definitions
|
|
45
|
+
BUILTIN_TEMPLATES = {
|
|
46
|
+
"quick": {
|
|
47
|
+
"name": "quick",
|
|
48
|
+
"description": "快速记录想法",
|
|
49
|
+
"note_type": "fleeting",
|
|
50
|
+
"title_format": "{{date}}-{{title}}",
|
|
51
|
+
"content": '## 快速笔记\n\n创建于 {{datetime}}\n\n{{content}}',
|
|
52
|
+
"tags": ["fleeting"],
|
|
53
|
+
},
|
|
54
|
+
"meeting": {
|
|
55
|
+
"name": "meeting",
|
|
56
|
+
"description": "会议记录模板",
|
|
57
|
+
"note_type": "permanent",
|
|
58
|
+
"title_format": "{{date}}-{{title}}",
|
|
59
|
+
"content": '## 会议信息\n- **日期**: {{date}}\n- **时间**: {{time}}\n\n## 参会人员\n\n## 议程\n\n## 会议内容\n\n{{content}}\n\n## 行动项\n- [ ] ',
|
|
60
|
+
"tags": ["meeting", "permanent"],
|
|
61
|
+
},
|
|
62
|
+
"literature": {
|
|
63
|
+
"name": "literature",
|
|
64
|
+
"description": "阅读笔记模板",
|
|
65
|
+
"note_type": "literature",
|
|
66
|
+
"title_format": "{{title}}",
|
|
67
|
+
"content": '## 文献信息\n- **来源**: {{source}}\n- **作者**: \n- **阅读日期**: {{date}}\n\n## 核心观点\n\n## 个人思考\n\n## 关联笔记\n\n{{content}}',
|
|
68
|
+
"tags": ["literature"],
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def __init__(self, templates_dir: Path):
|
|
73
|
+
"""
|
|
74
|
+
Initialize template manager
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
templates_dir: Directory to store templates
|
|
78
|
+
"""
|
|
79
|
+
self.templates_dir = templates_dir
|
|
80
|
+
self._ensure_templates_dir()
|
|
81
|
+
self._ensure_builtin_templates()
|
|
82
|
+
|
|
83
|
+
def _ensure_templates_dir(self) -> None:
|
|
84
|
+
"""Create templates directory if it doesn't exist"""
|
|
85
|
+
self.templates_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
def _ensure_builtin_templates(self) -> None:
|
|
88
|
+
"""Create built-in templates if they don't exist"""
|
|
89
|
+
for name, template_data in self.BUILTIN_TEMPLATES.items():
|
|
90
|
+
template_path = self.templates_dir / f"{name}.yaml"
|
|
91
|
+
if not template_path.exists():
|
|
92
|
+
self._save_template_file(template_path, template_data, is_builtin=True)
|
|
93
|
+
|
|
94
|
+
def _save_template_file(self, path: Path, data: Dict[str, Any], is_builtin: bool = False) -> None:
|
|
95
|
+
"""Save template to YAML file"""
|
|
96
|
+
template_data = {
|
|
97
|
+
"name": data["name"],
|
|
98
|
+
"description": data["description"],
|
|
99
|
+
"note_type": data["note_type"],
|
|
100
|
+
"title_format": data["title_format"],
|
|
101
|
+
"content": data["content"],
|
|
102
|
+
"tags": data.get("tags", []),
|
|
103
|
+
"is_builtin": is_builtin,
|
|
104
|
+
}
|
|
105
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
106
|
+
yaml.dump(template_data, f, allow_unicode=True, sort_keys=False)
|
|
107
|
+
|
|
108
|
+
def list_templates(self) -> List[NoteTemplate]:
|
|
109
|
+
"""
|
|
110
|
+
List all available templates
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of NoteTemplate objects
|
|
114
|
+
"""
|
|
115
|
+
templates = []
|
|
116
|
+
|
|
117
|
+
# Scan templates directory for YAML files
|
|
118
|
+
for template_file in self.templates_dir.glob("*.yaml"):
|
|
119
|
+
try:
|
|
120
|
+
template = self._load_template_file(template_file)
|
|
121
|
+
if template:
|
|
122
|
+
templates.append(template)
|
|
123
|
+
except Exception:
|
|
124
|
+
# Skip invalid template files
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
# Sort by name
|
|
128
|
+
templates.sort(key=lambda t: t.name)
|
|
129
|
+
return templates
|
|
130
|
+
|
|
131
|
+
def _load_template_file(self, path: Path) -> Optional[NoteTemplate]:
|
|
132
|
+
"""Load a single template from file"""
|
|
133
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
134
|
+
data = yaml.safe_load(f)
|
|
135
|
+
|
|
136
|
+
if not data:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
return NoteTemplate(
|
|
140
|
+
name=data.get("name", path.stem),
|
|
141
|
+
description=data.get("description", ""),
|
|
142
|
+
note_type=data.get("note_type", "fleeting"),
|
|
143
|
+
title_format=data.get("title_format", "{{title}}"),
|
|
144
|
+
content=data.get("content", ""),
|
|
145
|
+
tags=data.get("tags", []),
|
|
146
|
+
is_builtin=data.get("is_builtin", False),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def get_template(self, name: str) -> Optional[NoteTemplate]:
|
|
150
|
+
"""
|
|
151
|
+
Get a template by name
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Template name
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
NoteTemplate or None if not found
|
|
158
|
+
"""
|
|
159
|
+
template_path = self.templates_dir / f"{name}.yaml"
|
|
160
|
+
|
|
161
|
+
if not template_path.exists():
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
return self._load_template_file(template_path)
|
|
165
|
+
|
|
166
|
+
def render(self, name: str, variables: Dict[str, Any]) -> Dict[str, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Render a template with variables
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
name: Template name
|
|
172
|
+
variables: Variables to use in rendering
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dict with rendered title, content, note_type, and tags
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
TemplateNotFoundError: If template doesn't exist
|
|
179
|
+
TemplateRenderError: If template rendering fails
|
|
180
|
+
"""
|
|
181
|
+
template = self.get_template(name)
|
|
182
|
+
|
|
183
|
+
if not template:
|
|
184
|
+
available = [t.name for t in self.list_templates()]
|
|
185
|
+
raise TemplateNotFoundError(
|
|
186
|
+
f"Template '{name}' not found. "
|
|
187
|
+
f"Available templates: {', '.join(available) if available else 'none'}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Merge provided variables with defaults
|
|
191
|
+
render_vars = self._get_default_variables()
|
|
192
|
+
render_vars.update(variables)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Render title
|
|
196
|
+
title_template = Template(template.title_format)
|
|
197
|
+
rendered_title = title_template.render(**render_vars)
|
|
198
|
+
|
|
199
|
+
# Render content
|
|
200
|
+
content_template = Template(template.content)
|
|
201
|
+
rendered_content = content_template.render(**render_vars)
|
|
202
|
+
|
|
203
|
+
except UndefinedError as e:
|
|
204
|
+
raise TemplateRenderError(f"Template variable undefined: {e}")
|
|
205
|
+
except Exception as e:
|
|
206
|
+
raise TemplateRenderError(f"Failed to render template: {e}")
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"title": rendered_title,
|
|
210
|
+
"content": rendered_content,
|
|
211
|
+
"note_type": template.note_type,
|
|
212
|
+
"tags": template.tags.copy(),
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
def _get_default_variables(self) -> Dict[str, str]:
|
|
216
|
+
"""Get default template variables (date, time, etc.)"""
|
|
217
|
+
now = datetime.now()
|
|
218
|
+
return {
|
|
219
|
+
"date": now.strftime("%Y-%m-%d"),
|
|
220
|
+
"time": now.strftime("%H:%M"),
|
|
221
|
+
"datetime": now.strftime("%Y-%m-%d %H:%M"),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def get_available_templates(self) -> List[str]:
|
|
225
|
+
"""Get list of available template names"""
|
|
226
|
+
return [t.name for t in self.list_templates()]
|
|
227
|
+
|
|
228
|
+
def create_template(self, name: str, description: str, note_type: str,
|
|
229
|
+
title_format: str, content: str, tags: List[str]) -> NoteTemplate:
|
|
230
|
+
"""
|
|
231
|
+
Create a new custom template
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
name: Template name
|
|
235
|
+
description: Template description
|
|
236
|
+
note_type: Note type (fleeting/literature/permanent)
|
|
237
|
+
title_format: Title format with variables
|
|
238
|
+
content: Template content
|
|
239
|
+
tags: Default tags
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Created NoteTemplate
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
TemplateError: If template already exists
|
|
246
|
+
"""
|
|
247
|
+
template_path = self.templates_dir / f"{name}.yaml"
|
|
248
|
+
|
|
249
|
+
if template_path.exists():
|
|
250
|
+
raise TemplateError(f"Template '{name}' already exists")
|
|
251
|
+
|
|
252
|
+
template_data = {
|
|
253
|
+
"name": name,
|
|
254
|
+
"description": description,
|
|
255
|
+
"note_type": note_type,
|
|
256
|
+
"title_format": title_format,
|
|
257
|
+
"content": content,
|
|
258
|
+
"tags": tags,
|
|
259
|
+
"is_builtin": False,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
self._save_template_file(template_path, template_data, is_builtin=False)
|
|
263
|
+
|
|
264
|
+
return NoteTemplate(
|
|
265
|
+
name=name,
|
|
266
|
+
description=description,
|
|
267
|
+
note_type=note_type,
|
|
268
|
+
title_format=title_format,
|
|
269
|
+
content=content,
|
|
270
|
+
tags=tags,
|
|
271
|
+
is_builtin=False,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def delete_template(self, name: str) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Delete a custom template
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
name: Template name
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
TemplateNotFoundError: If template doesn't exist
|
|
283
|
+
TemplateError: If trying to delete a built-in template
|
|
284
|
+
"""
|
|
285
|
+
template = self.get_template(name)
|
|
286
|
+
|
|
287
|
+
if not template:
|
|
288
|
+
raise TemplateNotFoundError(f"Template '{name}' not found")
|
|
289
|
+
|
|
290
|
+
if template.is_builtin:
|
|
291
|
+
raise TemplateError(f"Cannot remove built-in template '{name}'")
|
|
292
|
+
|
|
293
|
+
template_path = self.templates_dir / f"{name}.yaml"
|
|
294
|
+
template_path.unlink()
|
|
295
|
+
|
|
296
|
+
def get_template_path(self, name: str) -> Optional[Path]:
|
|
297
|
+
"""Get the file path of a template"""
|
|
298
|
+
template_path = self.templates_dir / f"{name}.yaml"
|
|
299
|
+
if template_path.exists():
|
|
300
|
+
return template_path
|
|
301
|
+
return None
|
jfox/template_cli.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Template management CLI commands"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from .config import config
|
|
13
|
+
from .template import TemplateManager, TemplateError, TemplateNotFoundError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Create template subcommand app
|
|
19
|
+
template_app = typer.Typer(
|
|
20
|
+
name="template",
|
|
21
|
+
help="Manage note templates",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_template_manager() -> TemplateManager:
|
|
26
|
+
"""Get template manager instance for current knowledge base"""
|
|
27
|
+
templates_dir = config.base_dir / ".zk" / "templates"
|
|
28
|
+
return TemplateManager(templates_dir)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@template_app.command("list")
|
|
32
|
+
def list_templates(
|
|
33
|
+
kb: Optional[str] = typer.Option(None, "--kb", "-k", help="目标知识库名称"),
|
|
34
|
+
output_format: str = typer.Option("table", "--format", "-f", help="输出格式: json, table"),
|
|
35
|
+
json_output: bool = typer.Option(False, "--json", help="JSON 输出(快捷方式,等同于 --format json)"),
|
|
36
|
+
):
|
|
37
|
+
"""列出所有可用模板"""
|
|
38
|
+
try:
|
|
39
|
+
from .config import use_kb
|
|
40
|
+
|
|
41
|
+
# 处理 --json 快捷方式
|
|
42
|
+
if json_output:
|
|
43
|
+
output_format = "json"
|
|
44
|
+
|
|
45
|
+
if kb:
|
|
46
|
+
with use_kb(kb):
|
|
47
|
+
manager = get_template_manager()
|
|
48
|
+
templates = manager.list_templates()
|
|
49
|
+
else:
|
|
50
|
+
manager = get_template_manager()
|
|
51
|
+
templates = manager.list_templates()
|
|
52
|
+
|
|
53
|
+
# Separate built-in and custom templates
|
|
54
|
+
builtin_templates = [t for t in templates if t.is_builtin]
|
|
55
|
+
custom_templates = [t for t in templates if not t.is_builtin]
|
|
56
|
+
|
|
57
|
+
if output_format == "json":
|
|
58
|
+
import json
|
|
59
|
+
result = {
|
|
60
|
+
"builtin": [
|
|
61
|
+
{
|
|
62
|
+
"name": t.name,
|
|
63
|
+
"description": t.description,
|
|
64
|
+
"note_type": t.note_type,
|
|
65
|
+
}
|
|
66
|
+
for t in builtin_templates
|
|
67
|
+
],
|
|
68
|
+
"custom": [
|
|
69
|
+
{
|
|
70
|
+
"name": t.name,
|
|
71
|
+
"description": t.description,
|
|
72
|
+
"note_type": t.note_type,
|
|
73
|
+
}
|
|
74
|
+
for t in custom_templates
|
|
75
|
+
],
|
|
76
|
+
}
|
|
77
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
78
|
+
elif output_format == "table":
|
|
79
|
+
if builtin_templates:
|
|
80
|
+
console.print("[bold]Built-in Templates:[/bold]")
|
|
81
|
+
table = Table()
|
|
82
|
+
table.add_column("Name", style="cyan")
|
|
83
|
+
table.add_column("Description", style="white")
|
|
84
|
+
table.add_column("Type", style="green")
|
|
85
|
+
|
|
86
|
+
for t in builtin_templates:
|
|
87
|
+
table.add_row(t.name, t.description, t.note_type)
|
|
88
|
+
console.print(table)
|
|
89
|
+
console.print()
|
|
90
|
+
|
|
91
|
+
if custom_templates:
|
|
92
|
+
console.print("[bold]Custom Templates:[/bold]")
|
|
93
|
+
table = Table()
|
|
94
|
+
table.add_column("Name", style="cyan")
|
|
95
|
+
table.add_column("Description", style="white")
|
|
96
|
+
table.add_column("Type", style="green")
|
|
97
|
+
|
|
98
|
+
for t in custom_templates:
|
|
99
|
+
table.add_row(t.name, t.description, t.note_type)
|
|
100
|
+
console.print(table)
|
|
101
|
+
elif not builtin_templates:
|
|
102
|
+
console.print("[dim]No templates found[/dim]")
|
|
103
|
+
else:
|
|
104
|
+
console.print(f"[red]Error:[/red] Unsupported format: {output_format}")
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
109
|
+
raise typer.Exit(1)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@template_app.command("show")
|
|
113
|
+
def show_template(
|
|
114
|
+
name: str = typer.Argument(..., help="模板名称"),
|
|
115
|
+
kb: Optional[str] = typer.Option(None, "--kb", "-k", help="目标知识库名称"),
|
|
116
|
+
json_output: bool = typer.Option(True, "--json/--no-json", help="JSON 输出"),
|
|
117
|
+
):
|
|
118
|
+
"""查看模板详情"""
|
|
119
|
+
try:
|
|
120
|
+
from .config import use_kb
|
|
121
|
+
|
|
122
|
+
if kb:
|
|
123
|
+
with use_kb(kb):
|
|
124
|
+
manager = get_template_manager()
|
|
125
|
+
template = manager.get_template(name)
|
|
126
|
+
else:
|
|
127
|
+
manager = get_template_manager()
|
|
128
|
+
template = manager.get_template(name)
|
|
129
|
+
|
|
130
|
+
if not template:
|
|
131
|
+
available = manager.get_available_templates()
|
|
132
|
+
console.print(f"[red]Template '{name}' not found[/red]")
|
|
133
|
+
if available:
|
|
134
|
+
console.print(f"[dim]Available: {', '.join(available)}[/dim]")
|
|
135
|
+
raise typer.Exit(1)
|
|
136
|
+
|
|
137
|
+
if json_output:
|
|
138
|
+
import json
|
|
139
|
+
result = {
|
|
140
|
+
"name": template.name,
|
|
141
|
+
"description": template.description,
|
|
142
|
+
"note_type": template.note_type,
|
|
143
|
+
"title_format": template.title_format,
|
|
144
|
+
"content": template.content,
|
|
145
|
+
"tags": template.tags,
|
|
146
|
+
"is_builtin": template.is_builtin,
|
|
147
|
+
}
|
|
148
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
149
|
+
else:
|
|
150
|
+
builtin_tag = " (built-in)" if template.is_builtin else ""
|
|
151
|
+
console.print(f"[bold]{template.name}{builtin_tag}[/bold]")
|
|
152
|
+
console.print(f"[dim]{template.description}[/dim]")
|
|
153
|
+
console.print()
|
|
154
|
+
console.print(f"Type: {template.note_type}")
|
|
155
|
+
console.print(f"Title Format: {template.title_format}")
|
|
156
|
+
console.print(f"Tags: {', '.join(template.tags) if template.tags else 'none'}")
|
|
157
|
+
console.print()
|
|
158
|
+
console.print("[bold]Content:[/bold]")
|
|
159
|
+
console.print(template.content)
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@template_app.command("create")
|
|
167
|
+
def create_template(
|
|
168
|
+
name: str = typer.Argument(..., help="模板名称"),
|
|
169
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="模板描述"),
|
|
170
|
+
note_type: str = typer.Option("fleeting", "--type", "-t", help="笔记类型 (fleeting/literature/permanent)"),
|
|
171
|
+
title_format: str = typer.Option("{{date}}-{{title}}", "--title-format", help="标题格式"),
|
|
172
|
+
content: Optional[str] = typer.Option(None, "--content", "-c", help="模板内容"),
|
|
173
|
+
tags: Optional[List[str]] = typer.Option(None, "--tag", help="默认标签"),
|
|
174
|
+
kb: Optional[str] = typer.Option(None, "--kb", "-k", help="目标知识库名称"),
|
|
175
|
+
force: bool = typer.Option(False, "--force", "-f", help="覆盖已存在模板"),
|
|
176
|
+
json_output: bool = typer.Option(True, "--json/--no-json", help="JSON 输出"),
|
|
177
|
+
):
|
|
178
|
+
"""创建新模板"""
|
|
179
|
+
try:
|
|
180
|
+
from .config import use_kb
|
|
181
|
+
|
|
182
|
+
if kb:
|
|
183
|
+
with use_kb(kb):
|
|
184
|
+
manager = get_template_manager()
|
|
185
|
+
else:
|
|
186
|
+
manager = get_template_manager()
|
|
187
|
+
|
|
188
|
+
# Check if template exists
|
|
189
|
+
existing = manager.get_template(name)
|
|
190
|
+
if existing and not force:
|
|
191
|
+
console.print(f"[red]Template '{name}' already exists[/red]")
|
|
192
|
+
console.print("[dim]Use --force to overwrite[/dim]")
|
|
193
|
+
raise typer.Exit(1)
|
|
194
|
+
|
|
195
|
+
# Interactive mode if not all required fields provided
|
|
196
|
+
desc = description or typer.prompt("Description", default=f"Custom {name} template")
|
|
197
|
+
content_str = content or typer.prompt(
|
|
198
|
+
"Content (use {{variable}} for variables)",
|
|
199
|
+
default="## {{title}}\n\n{{content}}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Validate note_type
|
|
203
|
+
if note_type not in ["fleeting", "literature", "permanent"]:
|
|
204
|
+
console.print(f"[red]Invalid note type: {note_type}[/red]")
|
|
205
|
+
raise typer.Exit(1)
|
|
206
|
+
|
|
207
|
+
template = manager.create_template(
|
|
208
|
+
name=name,
|
|
209
|
+
description=desc,
|
|
210
|
+
note_type=note_type,
|
|
211
|
+
title_format=title_format,
|
|
212
|
+
content=content_str,
|
|
213
|
+
tags=tags or [],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if json_output:
|
|
217
|
+
import json
|
|
218
|
+
result = {
|
|
219
|
+
"success": True,
|
|
220
|
+
"template": {
|
|
221
|
+
"name": template.name,
|
|
222
|
+
"description": template.description,
|
|
223
|
+
"note_type": template.note_type,
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
227
|
+
else:
|
|
228
|
+
action = "updated" if existing else "created"
|
|
229
|
+
console.print(f"[green]Template '{name}' {action} successfully[/green]")
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@template_app.command("edit")
|
|
237
|
+
def edit_template(
|
|
238
|
+
name: str = typer.Argument(..., help="模板名称"),
|
|
239
|
+
kb: Optional[str] = typer.Option(None, "--kb", "-k", help="目标知识库名称"),
|
|
240
|
+
):
|
|
241
|
+
"""编辑模板(使用系统默认编辑器)"""
|
|
242
|
+
try:
|
|
243
|
+
from .config import use_kb
|
|
244
|
+
|
|
245
|
+
if kb:
|
|
246
|
+
with use_kb(kb):
|
|
247
|
+
manager = get_template_manager()
|
|
248
|
+
template = manager.get_template(name)
|
|
249
|
+
else:
|
|
250
|
+
manager = get_template_manager()
|
|
251
|
+
template = manager.get_template(name)
|
|
252
|
+
|
|
253
|
+
if not template:
|
|
254
|
+
console.print(f"[red]Template '{name}' not found[/red]")
|
|
255
|
+
raise typer.Exit(1)
|
|
256
|
+
|
|
257
|
+
if template.is_builtin:
|
|
258
|
+
console.print(f"[red]Cannot edit built-in template '{name}'[/red]")
|
|
259
|
+
console.print("[dim]Create a custom template instead[/dim]")
|
|
260
|
+
raise typer.Exit(1)
|
|
261
|
+
|
|
262
|
+
template_path = manager.get_template_path(name)
|
|
263
|
+
if not template_path:
|
|
264
|
+
console.print(f"[red]Template file not found[/red]")
|
|
265
|
+
raise typer.Exit(1)
|
|
266
|
+
|
|
267
|
+
# Get editor from environment
|
|
268
|
+
editor = os.environ.get("EDITOR", "notepad" if os.name == "nt" else "vi")
|
|
269
|
+
|
|
270
|
+
# Open editor
|
|
271
|
+
subprocess.run([editor, str(template_path)], check=True)
|
|
272
|
+
console.print(f"[green]Template '{name}' updated[/green]")
|
|
273
|
+
|
|
274
|
+
except subprocess.CalledProcessError as e:
|
|
275
|
+
console.print(f"[red]Editor failed: {e}[/red]")
|
|
276
|
+
raise typer.Exit(1)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
279
|
+
raise typer.Exit(1)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@template_app.command("remove")
|
|
283
|
+
def remove_template(
|
|
284
|
+
name: str = typer.Argument(..., help="模板名称"),
|
|
285
|
+
kb: Optional[str] = typer.Option(None, "--kb", "-k", help="目标知识库名称"),
|
|
286
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认"),
|
|
287
|
+
json_output: bool = typer.Option(True, "--json/--no-json", help="JSON 输出"),
|
|
288
|
+
):
|
|
289
|
+
"""删除自定义模板"""
|
|
290
|
+
try:
|
|
291
|
+
from .config import use_kb
|
|
292
|
+
|
|
293
|
+
if kb:
|
|
294
|
+
with use_kb(kb):
|
|
295
|
+
manager = get_template_manager()
|
|
296
|
+
else:
|
|
297
|
+
manager = get_template_manager()
|
|
298
|
+
|
|
299
|
+
template = manager.get_template(name)
|
|
300
|
+
if not template:
|
|
301
|
+
console.print(f"[red]Template '{name}' not found[/red]")
|
|
302
|
+
raise typer.Exit(1)
|
|
303
|
+
|
|
304
|
+
if template.is_builtin:
|
|
305
|
+
console.print(f"[red]Cannot remove built-in template '{name}'[/red]")
|
|
306
|
+
raise typer.Exit(1)
|
|
307
|
+
|
|
308
|
+
# Confirm deletion
|
|
309
|
+
if not yes:
|
|
310
|
+
if not json_output:
|
|
311
|
+
confirm = input(f"Delete template '{name}'? (y/N): ")
|
|
312
|
+
if confirm.lower() != "y":
|
|
313
|
+
console.print("Cancelled")
|
|
314
|
+
raise typer.Exit(0)
|
|
315
|
+
|
|
316
|
+
manager.delete_template(name)
|
|
317
|
+
|
|
318
|
+
if json_output:
|
|
319
|
+
import json
|
|
320
|
+
result = {"success": True, "deleted": name}
|
|
321
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
322
|
+
else:
|
|
323
|
+
console.print(f"[green]Template '{name}' deleted[/green]")
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
327
|
+
raise typer.Exit(1)
|