acm-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.
- acm/__init__.py +3 -0
- acm/cli.py +472 -0
- acm/config.py +272 -0
- acm/feedback.py +148 -0
- acm/generator.py +127 -0
- acm/git.py +193 -0
- acm/llm/__init__.py +25 -0
- acm/llm/base.py +58 -0
- acm/llm/openai_compat.py +78 -0
- acm/llm/qwen.py +18 -0
- acm/prompt.py +178 -0
- acm/splitter.py +92 -0
- acm/ui.py +279 -0
- acm_cli-0.1.0.dist-info/METADATA +369 -0
- acm_cli-0.1.0.dist-info/RECORD +18 -0
- acm_cli-0.1.0.dist-info/WHEEL +4 -0
- acm_cli-0.1.0.dist-info/entry_points.txt +2 -0
- acm_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
acm/__init__.py
ADDED
acm/cli.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""CLI 入口:click 命令组,默认命令、init、config 子命令。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from acm import __version__
|
|
11
|
+
from acm.config import (
|
|
12
|
+
ACMConfig,
|
|
13
|
+
CommitConfig,
|
|
14
|
+
ConfigError,
|
|
15
|
+
GLOBAL_CONFIG_DIR,
|
|
16
|
+
GLOBAL_CONFIG_FILE,
|
|
17
|
+
LLMConfig,
|
|
18
|
+
PROJECT_CONFIG_FILE,
|
|
19
|
+
PROVIDER_DEFAULTS,
|
|
20
|
+
config_get,
|
|
21
|
+
config_set,
|
|
22
|
+
load_config,
|
|
23
|
+
save_config,
|
|
24
|
+
validate_config,
|
|
25
|
+
)
|
|
26
|
+
from acm.feedback import add_feedback, clear_feedback, load_feedback
|
|
27
|
+
from acm.generator import CommitGenerator, GenerateError
|
|
28
|
+
from acm.git import (
|
|
29
|
+
DiffResult,
|
|
30
|
+
GitError,
|
|
31
|
+
add_all,
|
|
32
|
+
build_diff_result,
|
|
33
|
+
commit,
|
|
34
|
+
commit_files,
|
|
35
|
+
get_staged_files,
|
|
36
|
+
has_unstaged_changes,
|
|
37
|
+
has_untracked_files,
|
|
38
|
+
is_git_repo,
|
|
39
|
+
)
|
|
40
|
+
from acm.llm import create_llm
|
|
41
|
+
from acm.splitter import CommitSplitter, SplitError
|
|
42
|
+
from acm.ui import (
|
|
43
|
+
console,
|
|
44
|
+
print_error,
|
|
45
|
+
print_info,
|
|
46
|
+
print_success,
|
|
47
|
+
print_verbose,
|
|
48
|
+
print_warning,
|
|
49
|
+
prompt_action,
|
|
50
|
+
prompt_confirm,
|
|
51
|
+
prompt_edit,
|
|
52
|
+
prompt_feedback,
|
|
53
|
+
show_commit_message,
|
|
54
|
+
show_diff_summary,
|
|
55
|
+
show_split_suggestion,
|
|
56
|
+
spinner,
|
|
57
|
+
stream_commit_message,
|
|
58
|
+
warn_large_commit,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
LARGE_COMMIT_THRESHOLD = 20
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.group(invoke_without_command=True)
|
|
66
|
+
@click.option("--dry-run", is_flag=True, help="仅生成 message,不执行 git commit")
|
|
67
|
+
@click.option("--verbose", is_flag=True, help="输出详细调试信息")
|
|
68
|
+
@click.option("--split", is_flag=True, help="智能拆分为多个原子提交")
|
|
69
|
+
@click.version_option(__version__, prog_name="acm")
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def main(ctx: click.Context, dry_run: bool, verbose: bool, split: bool) -> None:
|
|
72
|
+
"""ACM - 基于 LLM 的 Git Commit Message 自动生成工具。"""
|
|
73
|
+
ctx.ensure_object(dict)
|
|
74
|
+
ctx.obj["verbose"] = verbose
|
|
75
|
+
|
|
76
|
+
if ctx.invoked_subcommand is not None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# 主流程:生成 commit message
|
|
80
|
+
_run_generate(dry_run=dry_run, verbose=verbose, split_mode=split)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _run_generate(dry_run: bool, verbose: bool, split_mode: bool) -> None:
|
|
84
|
+
"""主流程:分析 staged 内容,生成 commit message。"""
|
|
85
|
+
# 1. 检查 Git 仓库
|
|
86
|
+
if not is_git_repo():
|
|
87
|
+
print_error("当前目录不是 Git 仓库,请在 Git 项目根目录下运行")
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
# 2. 检查 staged 文件
|
|
91
|
+
try:
|
|
92
|
+
staged_files = get_staged_files()
|
|
93
|
+
except GitError as e:
|
|
94
|
+
print_error(str(e))
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
if not staged_files:
|
|
98
|
+
has_unstaged = has_unstaged_changes()
|
|
99
|
+
has_untracked = has_untracked_files()
|
|
100
|
+
if has_unstaged or has_untracked:
|
|
101
|
+
if has_unstaged and has_untracked:
|
|
102
|
+
print_warning("暂存区为空,但检测到未暂存的修改和未跟踪的新文件")
|
|
103
|
+
elif has_unstaged:
|
|
104
|
+
print_warning("暂存区为空,但检测到未暂存的修改")
|
|
105
|
+
else:
|
|
106
|
+
print_warning("暂存区为空,但检测到未跟踪的新文件")
|
|
107
|
+
|
|
108
|
+
if prompt_confirm("是否执行 git add . 将所有变更添加到暂存区并继续?"):
|
|
109
|
+
try:
|
|
110
|
+
add_all()
|
|
111
|
+
print_success("已执行 git add -A,所有变更已添加到暂存区")
|
|
112
|
+
# 重新获取 staged 文件并继续流程
|
|
113
|
+
staged_files = get_staged_files()
|
|
114
|
+
if not staged_files:
|
|
115
|
+
print_error("执行 git add 后暂存区仍为空")
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
except GitError as e:
|
|
118
|
+
print_error(f"执行 git add 失败: {e}")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
else:
|
|
121
|
+
print_info("已取消,请手动执行 git add 后再重试")
|
|
122
|
+
sys.exit(0)
|
|
123
|
+
else:
|
|
124
|
+
print_error("暂存区为空,没有需要提交的变更")
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
# 3. 大提交警告
|
|
128
|
+
if len(staged_files) > LARGE_COMMIT_THRESHOLD:
|
|
129
|
+
if not warn_large_commit(len(staged_files)):
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
# 4. 加载配置
|
|
133
|
+
try:
|
|
134
|
+
config = load_config()
|
|
135
|
+
validate_config(config)
|
|
136
|
+
except ConfigError as e:
|
|
137
|
+
print_error(str(e))
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
if verbose:
|
|
141
|
+
_show_verbose_config(config)
|
|
142
|
+
|
|
143
|
+
# 5. 获取 diff
|
|
144
|
+
try:
|
|
145
|
+
diff_result = build_diff_result(max_diff_lines=config.commit.max_diff_lines)
|
|
146
|
+
except GitError as e:
|
|
147
|
+
print_error(str(e))
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
show_diff_summary(diff_result)
|
|
151
|
+
|
|
152
|
+
# 6. 创建 LLM 实例
|
|
153
|
+
try:
|
|
154
|
+
llm = create_llm(config)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print_error(f"创建 LLM 实例失败: {e}")
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
|
|
159
|
+
# 7. 智能拆分模式
|
|
160
|
+
if split_mode:
|
|
161
|
+
_run_split_mode(llm, config, diff_result, staged_files, dry_run, verbose)
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# 8. 常规生成模式(流式输出)
|
|
165
|
+
generator = CommitGenerator(llm, config)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
stream = generator.stream_generate(diff_result.diff_content)
|
|
169
|
+
message = stream_commit_message(stream)
|
|
170
|
+
except (GenerateError, Exception) as e:
|
|
171
|
+
print_error(f"LLM 接口调用失败: {e}")
|
|
172
|
+
if verbose:
|
|
173
|
+
import traceback
|
|
174
|
+
print_verbose(traceback.format_exc())
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
if verbose:
|
|
178
|
+
print_verbose(f"LLM 原始响应: {message}")
|
|
179
|
+
|
|
180
|
+
show_commit_message(message)
|
|
181
|
+
|
|
182
|
+
if dry_run:
|
|
183
|
+
print_info("(dry-run 模式,不执行 git commit)")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# 9. 交互循环
|
|
187
|
+
_interaction_loop(generator, diff_result, message, verbose)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _interaction_loop(
|
|
191
|
+
generator: CommitGenerator,
|
|
192
|
+
diff_result: DiffResult,
|
|
193
|
+
message: str,
|
|
194
|
+
verbose: bool,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""用户交互循环:确认/编辑/反馈/放弃。"""
|
|
197
|
+
while True:
|
|
198
|
+
action = prompt_action()
|
|
199
|
+
|
|
200
|
+
if action == "y":
|
|
201
|
+
_do_commit(message)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
elif action == "e":
|
|
205
|
+
previous_message = message
|
|
206
|
+
message = prompt_edit(message)
|
|
207
|
+
# 记录编辑偏好(仅当用户实际修改了 message 时)
|
|
208
|
+
if message != previous_message:
|
|
209
|
+
try:
|
|
210
|
+
add_feedback(previous_message, "用户手动编辑", message)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
show_commit_message(message)
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
elif action == "r":
|
|
217
|
+
feedback = prompt_feedback()
|
|
218
|
+
if not feedback:
|
|
219
|
+
print_warning("未输入反馈内容,请重新选择操作")
|
|
220
|
+
show_commit_message(message)
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
previous_message = message
|
|
224
|
+
try:
|
|
225
|
+
stream = generator.stream_regenerate(feedback)
|
|
226
|
+
message = stream_commit_message(stream)
|
|
227
|
+
except (GenerateError, Exception) as e:
|
|
228
|
+
print_error(f"重新生成失败: {e}")
|
|
229
|
+
if verbose:
|
|
230
|
+
import traceback
|
|
231
|
+
print_verbose(traceback.format_exc())
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
# 记录反馈到本地,用于后续 prompt 优化
|
|
235
|
+
try:
|
|
236
|
+
add_feedback(previous_message, feedback, message)
|
|
237
|
+
except Exception:
|
|
238
|
+
pass # 反馈记录失败不影响主流程
|
|
239
|
+
|
|
240
|
+
show_commit_message(message)
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
elif action == "q":
|
|
244
|
+
print_info("已放弃提交")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _do_commit(message: str) -> None:
|
|
249
|
+
"""执行 git commit。"""
|
|
250
|
+
try:
|
|
251
|
+
output = commit(message)
|
|
252
|
+
print_success("提交成功!")
|
|
253
|
+
console.print(f"[dim]{output.strip()}[/dim]")
|
|
254
|
+
except GitError as e:
|
|
255
|
+
print_error(f"提交失败: {e}")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _run_split_mode(
|
|
259
|
+
llm,
|
|
260
|
+
config: ACMConfig,
|
|
261
|
+
diff_result: DiffResult,
|
|
262
|
+
staged_files,
|
|
263
|
+
dry_run: bool,
|
|
264
|
+
verbose: bool,
|
|
265
|
+
) -> None:
|
|
266
|
+
"""智能拆分模式。"""
|
|
267
|
+
splitter = CommitSplitter(llm, config)
|
|
268
|
+
file_paths = [f.path for f in staged_files]
|
|
269
|
+
|
|
270
|
+
with spinner("分析变更,生成拆分建议..."):
|
|
271
|
+
try:
|
|
272
|
+
groups = splitter.split(diff_result.diff_content, file_paths)
|
|
273
|
+
except (SplitError, Exception) as e:
|
|
274
|
+
print_error(f"拆分分析失败: {e}")
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
|
|
277
|
+
show_split_suggestion(groups)
|
|
278
|
+
|
|
279
|
+
if dry_run:
|
|
280
|
+
print_info("(dry-run 模式,不执行 git commit)")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
if not prompt_confirm("是否按上述方案执行拆分提交?"):
|
|
284
|
+
print_info("已取消拆分提交")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
for i, group in enumerate(groups, 1):
|
|
288
|
+
try:
|
|
289
|
+
output = commit_files(group.files, group.message)
|
|
290
|
+
print_success(f"提交 {i}/{len(groups)} 完成: {group.message}")
|
|
291
|
+
except GitError as e:
|
|
292
|
+
print_error(f"提交 {i} 失败: {e}")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _show_verbose_config(config: ACMConfig) -> None:
|
|
297
|
+
"""verbose 模式下显示配置信息。"""
|
|
298
|
+
print_verbose("--- 当前配置 ---")
|
|
299
|
+
print_verbose(f"提供商: {config.llm.provider}")
|
|
300
|
+
print_verbose(f"模型: {config.llm.model}")
|
|
301
|
+
print_verbose(f"Base URL: {config.llm.base_url}")
|
|
302
|
+
print_verbose(f"API Key: {'*' * 8}...{config.llm.api_key[-4:]}" if len(config.llm.api_key) > 4 else "API Key: ***")
|
|
303
|
+
print_verbose(f"语言: {config.commit.language}")
|
|
304
|
+
print_verbose(f"Diff 截断阈值: {config.commit.max_diff_lines} 行")
|
|
305
|
+
print_verbose("---")
|
|
306
|
+
console.print()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# init 子命令
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
@main.command()
|
|
314
|
+
@click.option("--global", "is_global", is_flag=True, default=False, help="生成全局配置文件")
|
|
315
|
+
def init(is_global: bool) -> None:
|
|
316
|
+
"""交互式初始化配置。"""
|
|
317
|
+
console.print("[bold]ACM 配置初始化[/bold]")
|
|
318
|
+
console.print()
|
|
319
|
+
|
|
320
|
+
# 1. 选择提供商
|
|
321
|
+
providers = list(PROVIDER_DEFAULTS.keys()) + ["other"]
|
|
322
|
+
console.print("可选提供商:")
|
|
323
|
+
for i, p in enumerate(providers, 1):
|
|
324
|
+
console.print(f" {i}. {p}")
|
|
325
|
+
|
|
326
|
+
provider_idx = click.prompt("选择提供商", type=int, default=1) - 1
|
|
327
|
+
if provider_idx < 0 or provider_idx >= len(providers):
|
|
328
|
+
provider_idx = 0
|
|
329
|
+
|
|
330
|
+
provider = providers[provider_idx]
|
|
331
|
+
if provider == "other":
|
|
332
|
+
provider = click.prompt("输入提供商名称", type=str)
|
|
333
|
+
|
|
334
|
+
# 2. API Key
|
|
335
|
+
api_key = click.prompt("输入 API Key", type=str, hide_input=True)
|
|
336
|
+
|
|
337
|
+
# 3. 模型名称
|
|
338
|
+
model = click.prompt("输入模型名称 (必填)", type=str)
|
|
339
|
+
if not model:
|
|
340
|
+
print_error("模型名称不能为空")
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
|
|
343
|
+
# 4. Base URL(可选)
|
|
344
|
+
default_url = PROVIDER_DEFAULTS.get(provider, {}).get("base_url", "")
|
|
345
|
+
if default_url:
|
|
346
|
+
console.print(f"[dim]默认 Base URL: {default_url}[/dim]")
|
|
347
|
+
base_url = click.prompt("自定义 Base URL (留空使用默认)", type=str, default="")
|
|
348
|
+
|
|
349
|
+
# 5. 语言
|
|
350
|
+
language = click.prompt("输出语言", type=click.Choice(["zh-CN", "en"]), default="zh-CN")
|
|
351
|
+
|
|
352
|
+
# 构建配置
|
|
353
|
+
config = ACMConfig(
|
|
354
|
+
llm=LLMConfig(provider=provider, api_key=api_key, model=model, base_url=base_url),
|
|
355
|
+
commit=CommitConfig(language=language),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# 保存
|
|
359
|
+
if is_global:
|
|
360
|
+
path = GLOBAL_CONFIG_FILE
|
|
361
|
+
else:
|
|
362
|
+
path = Path.cwd() / PROJECT_CONFIG_FILE
|
|
363
|
+
|
|
364
|
+
save_config(config, path)
|
|
365
|
+
print_success(f"配置已保存到: {path}")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
# config 子命令
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
@main.group()
|
|
373
|
+
def config() -> None:
|
|
374
|
+
"""查看和修改配置项。"""
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@config.command("list")
|
|
379
|
+
def config_list() -> None:
|
|
380
|
+
"""列出当前生效的所有配置。"""
|
|
381
|
+
cfg = load_config()
|
|
382
|
+
console.print("[bold]当前生效配置:[/bold]")
|
|
383
|
+
console.print()
|
|
384
|
+
|
|
385
|
+
items = [
|
|
386
|
+
("llm.provider", cfg.llm.provider),
|
|
387
|
+
("llm.model", cfg.llm.model),
|
|
388
|
+
("llm.base_url", cfg.llm.base_url),
|
|
389
|
+
("llm.api_key", f"{'*' * 8}...{cfg.llm.api_key[-4:]}" if len(cfg.llm.api_key) > 4 else "***"),
|
|
390
|
+
("commit.language", cfg.commit.language),
|
|
391
|
+
("commit.max_diff_lines", str(cfg.commit.max_diff_lines)),
|
|
392
|
+
("commit.types", ", ".join(cfg.commit.types) if cfg.commit.types else "(默认)"),
|
|
393
|
+
("prompt.system", "(自定义)" if cfg.prompt.system else "(默认)"),
|
|
394
|
+
("prompt.extra", cfg.prompt.extra or "(无)"),
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
for key, value in items:
|
|
398
|
+
console.print(f" [cyan]{key}[/cyan] = {value}")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@config.command("get")
|
|
402
|
+
@click.argument("key")
|
|
403
|
+
def config_get_cmd(key: str) -> None:
|
|
404
|
+
"""查看某个配置项的值。"""
|
|
405
|
+
try:
|
|
406
|
+
cfg = load_config()
|
|
407
|
+
value = config_get(key, cfg)
|
|
408
|
+
console.print(value)
|
|
409
|
+
except ConfigError as e:
|
|
410
|
+
print_error(str(e))
|
|
411
|
+
sys.exit(1)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@config.command("set")
|
|
415
|
+
@click.argument("key")
|
|
416
|
+
@click.argument("value")
|
|
417
|
+
@click.option("--global", "is_global", is_flag=True, default=False, help="修改全局配置文件")
|
|
418
|
+
def config_set_cmd(key: str, value: str, is_global: bool) -> None:
|
|
419
|
+
"""修改某个配置项的值。"""
|
|
420
|
+
if is_global:
|
|
421
|
+
path = GLOBAL_CONFIG_FILE
|
|
422
|
+
else:
|
|
423
|
+
path = Path.cwd() / PROJECT_CONFIG_FILE
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
config_set(key, value, path)
|
|
427
|
+
print_success(f"已设置 {key} = {value} (文件: {path})")
|
|
428
|
+
except ConfigError as e:
|
|
429
|
+
print_error(str(e))
|
|
430
|
+
sys.exit(1)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
# feedback 子命令
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
@main.group()
|
|
438
|
+
def feedback() -> None:
|
|
439
|
+
"""管理反馈学习记录。"""
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@feedback.command("list")
|
|
444
|
+
def feedback_list() -> None:
|
|
445
|
+
"""列出所有历史反馈记录。"""
|
|
446
|
+
records = load_feedback()
|
|
447
|
+
if not records:
|
|
448
|
+
print_info("暂无反馈记录")
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
console.print(f"[bold]共 {len(records)} 条反馈记录:[/bold]")
|
|
452
|
+
console.print()
|
|
453
|
+
for i, r in enumerate(records, 1):
|
|
454
|
+
console.print(f" [cyan]#{i}[/cyan]")
|
|
455
|
+
console.print(f" 原始: [dim]{r.original_message}[/dim]")
|
|
456
|
+
console.print(f" 反馈: [yellow]{r.feedback}[/yellow]")
|
|
457
|
+
console.print(f" 改进: [green]{r.improved_message}[/green]")
|
|
458
|
+
console.print()
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@feedback.command("clear")
|
|
462
|
+
def feedback_clear() -> None:
|
|
463
|
+
"""清空所有反馈记录。"""
|
|
464
|
+
count = clear_feedback()
|
|
465
|
+
if count > 0:
|
|
466
|
+
print_success(f"已清除 {count} 条反馈记录")
|
|
467
|
+
else:
|
|
468
|
+
print_info("暂无反馈记录需要清除")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
if __name__ == "__main__":
|
|
472
|
+
main()
|