mcai-knowledge 0.2.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.
mcai/cli.py ADDED
@@ -0,0 +1,558 @@
1
+ """命令行入口。
2
+
3
+ 所有命令都支持 --home 指定工作区;未指定时按 MCAI_HOME → 当前目录 → 源码目录 的顺序解析。
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import json
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from . import pipeline, workspace
14
+ from .corpus import load_config
15
+
16
+ ROOT = workspace.DEV_ROOT
17
+
18
+
19
+ def _home(args: argparse.Namespace) -> Path:
20
+ return workspace.resolve_home(getattr(args, "home", None))
21
+
22
+
23
+ def cmd_init(args: argparse.Namespace) -> int:
24
+ result = workspace.init_workspace(args.path or ".", force=args.force)
25
+ print(f"✅ 工作区已初始化:{result['root']}")
26
+ for path in result["created"]:
27
+ print(f" + {path}")
28
+ for path in result["skipped"]:
29
+ print(f" = 已存在,跳过 {path}")
30
+ print("\n下一步:")
31
+ print(" 1. 把文档放进 corpus/,在 config/corpus.json 的 corpus 数组里登记")
32
+ print(" 2. mcai build")
33
+ print(" 3. mcai serve 或 mcai mcp")
34
+ return 0
35
+
36
+
37
+ def cmd_build(args: argparse.Namespace) -> int:
38
+ home = _home(args)
39
+ result = pipeline.rebuild(home)
40
+ print(f"✅ 构建完成:{result['concepts']} 个概念 / {result['faqs']} 组问答 / "
41
+ f"{result['relations']} 条关系 / {result['pages']} 个页面")
42
+ if result["conflicts"]:
43
+ print("⚠️ 定义冲突待裁定:" + "、".join(result["conflicts"]))
44
+ if result["coverage_missing"]:
45
+ print("⚠️ 词表覆盖缺失:" + "、".join(result["coverage_missing"]))
46
+ print(f"📄 站点:{home / 'dist' / 'index.html'}")
47
+ if args.serve:
48
+ from .server import serve
49
+ serve(home, args.port)
50
+ return 0
51
+
52
+
53
+ def _serve(args: argparse.Namespace) -> int:
54
+ from .server import serve
55
+ token = (getattr(args, "token", "") or os.environ.get("MCAI_TOKEN", "")).strip()
56
+ serve(_home(args), args.port, host=args.host, token=token, rate_limit=args.rate_limit,
57
+ autopilot_interval=getattr(args, "autopilot", 0))
58
+ return 0
59
+
60
+
61
+ def cmd_serve(args: argparse.Namespace) -> int:
62
+ return _serve(args)
63
+
64
+
65
+ def cmd_api(args: argparse.Namespace) -> int:
66
+ return _serve(args)
67
+
68
+
69
+ def cmd_mcp(args: argparse.Namespace) -> int:
70
+ from .mcp_server import main as mcp_main
71
+ mcp_main(_home(args))
72
+ return 0
73
+
74
+
75
+ def cmd_autopilot(args: argparse.Namespace) -> int:
76
+ from .autopilot import Autopilot
77
+ pilot = Autopilot(_home(args), port=args.port, ensure_server=not args.no_server)
78
+ if args.once:
79
+ outcome = pilot.run_once()
80
+ s = outcome["status"]
81
+ print(f"{'✅' if s['ok'] else '❌'} 检查完成 · 重建={outcome['rebuilt']} · "
82
+ f"{s['concepts']} 概念 / {s['faqs']} 问答 · 服务={'在线' if s['server_ok'] else '离线'}")
83
+ if s["error"]:
84
+ print(f" 错误:{s['error']}")
85
+ diff = outcome["diff"]
86
+ if diff["added"] or diff["removed"] or diff["changed"]:
87
+ print(f" 变更:+{len(diff['added'])} −{len(diff['removed'])} ~{len(diff['changed'])}")
88
+ print(f" 状态文件:{_home(args) / 'data' / 'status.json'}")
89
+ return 0 if s["ok"] else 1
90
+ try:
91
+ pilot.loop(args.interval)
92
+ except KeyboardInterrupt:
93
+ print("\n已停止。")
94
+ return 0
95
+
96
+
97
+ def cmd_quality(args: argparse.Namespace) -> int:
98
+ from . import quality
99
+ result = quality.inspect(_home(args))
100
+ counts = result["counts"]
101
+ print(f"质量巡检:{'✅ 通过' if result['ok'] else '❌ 有问题'} "
102
+ f"(错误 {counts['error']} · 警告 {counts['warn']} · 提示 {counts['info']})")
103
+ for issue in (result["errors"] + result["warnings"])[:20]:
104
+ mark = "❌" if issue["level"] == "error" else "⚠️"
105
+ print(f" {mark} [{issue['code']}] {issue['detail']}")
106
+ return 0 if result["ok"] else 1
107
+
108
+
109
+ def cmd_selftest(args: argparse.Namespace) -> int:
110
+ from . import selftest
111
+ result = selftest.run(_home(args))
112
+ mark = "✅" if result["ok"] else "❌"
113
+ print(f"接口自测:{mark} {result['passed']}/{result['total']} 通过")
114
+ for c in result["checks"]:
115
+ if not c["ok"]:
116
+ print(f" ❌ {c['name']} {c['detail']}")
117
+ return 0 if result["ok"] else 1
118
+
119
+
120
+ def cmd_snapshot(args: argparse.Namespace) -> int:
121
+ from . import snapshot
122
+ result = snapshot.create(_home(args), note=args.note, keep=args.keep,
123
+ with_corpus=not args.no_corpus)
124
+ m = result["manifest"]
125
+ print(f"✅ 快照已生成:{result['archive']}")
126
+ print(f" {m['files']} 个文件 · 原始 {m['bytes'] / 1024:.0f} KB · 压缩 {m['archive_bytes'] / 1024:.0f} KB")
127
+ if m.get("kb"):
128
+ kb = m["kb"]
129
+ print(f" 知识库:{kb.get('concepts')} 概念 / {kb.get('faqs')} 问答 / {kb.get('relations')} 关系")
130
+ if m.get("corpus"):
131
+ print(f" 语料:{len(m['corpus'])} 份(含在包内)")
132
+ if result["removed"]:
133
+ print(" 轮转删除:" + "、".join(result["removed"]))
134
+ return 0
135
+
136
+
137
+ def cmd_snapshots(args: argparse.Namespace) -> int:
138
+ from . import snapshot
139
+ rows = snapshot.list_snapshots(_home(args), limit=args.limit)
140
+ if not rows:
141
+ print("还没有快照。运行 mcai snapshot 生成第一份。")
142
+ return 0
143
+ print(f"{'时间':<26} {'大小':>8} {'概念':>4} {'问答':>4} 备注")
144
+ for r in rows:
145
+ kb = r.get("kb") or {}
146
+ print(f"{r['created_at']:<26} {r['bytes'] / 1024:>7.0f}K {kb.get('concepts', '-'):>4} "
147
+ f"{kb.get('faqs', '-'):>4} {r.get('note', '')}")
148
+ return 0
149
+
150
+
151
+ def cmd_restore(args: argparse.Namespace) -> int:
152
+ from . import snapshot
153
+ check = snapshot.verify(args.archive)
154
+ if not check.get("ok"):
155
+ print(f"❌ 快照损坏或不存在:{check.get('error') or check.get('broken')}")
156
+ return 1
157
+ result = snapshot.restore(args.archive, args.to, force=args.force)
158
+ if not result.get("ok"):
159
+ print(f"❌ {result.get('error')}")
160
+ return 1
161
+ print(f"✅ 已恢复到:{result['target']}({result['files']} 个条目)")
162
+ print(" 检查后可用:mcai build --home <该目录>,再 mcai serve --home <该目录>")
163
+ return 0
164
+
165
+
166
+ def cmd_dashboard(args: argparse.Namespace) -> int:
167
+ from . import dashboard
168
+ path = dashboard.render(_home(args))
169
+ print(f"✅ 观测台已生成:{path}")
170
+ return 0
171
+
172
+
173
+ def cmd_status(args: argparse.Namespace) -> int:
174
+ path = _home(args) / "data" / "status.json"
175
+ if not path.exists():
176
+ print("还没有状态文件:先运行 mcai autopilot --once")
177
+ return 1
178
+ status = json.loads(path.read_text(encoding="utf-8"))
179
+ print(f"检查时间:{status.get('checked_at')}")
180
+ print(f"健康:{'✅' if status.get('ok') else '❌'}")
181
+ print(f"概念 {status.get('concepts')} · 问答 {status.get('faqs')} · 关系 {status.get('relations')}")
182
+ print(f"服务:{'在线' if status.get('server_ok') else '离线'}"
183
+ f"(本轮启动:{'是' if status.get('started_server') else '否'})")
184
+ print(f"本轮重建:{'是' if status.get('rebuilt') else '否'}")
185
+ q = status.get("quality") or {}
186
+ t = status.get("selftest") or {}
187
+ if q:
188
+ print(f"质量巡检:{'✅' if q.get('ok') else '❌'} 错误 {q.get('errors', 0)} · 警告 {q.get('warnings', 0)}")
189
+ if t:
190
+ print(f"接口自测:{'✅' if t.get('ok') else '❌'} {t.get('passed', 0)}/{t.get('total', 0)}")
191
+ if status.get("error"):
192
+ print(f"错误:{status['error']}")
193
+ return 0 if status.get("ok") else 1
194
+
195
+
196
+ def cmd_token(args: argparse.Namespace) -> int:
197
+ from .deploy import new_token
198
+ token = new_token()
199
+ print(token)
200
+ print("\n用法:")
201
+ print(f" mcai serve --host 0.0.0.0 --token {token}")
202
+ print(f" mcai doctor --url <你的地址> --token {token}")
203
+ return 0
204
+
205
+
206
+ def cmd_set_url(args: argparse.Namespace) -> int:
207
+ from .deploy import set_site_url
208
+ home = _home(args)
209
+ try:
210
+ info = set_site_url(home, args.url)
211
+ except ValueError as exc:
212
+ print(f"❌ {exc}")
213
+ return 1
214
+ print(f"站点地址:{info['old'] or '(未设置)'} → {info['new']}")
215
+ if args.no_build:
216
+ print("已跳过重建(--no-build)")
217
+ return 0
218
+ result = pipeline.rebuild(home)
219
+ print(f"✅ 已重建:{result['concepts']} 概念 / {result['faqs']} 问答 / {result['pages']} 页面")
220
+ manifest = home / "dist" / ".well-known" / "mcp.json"
221
+ if manifest.exists():
222
+ base = json.loads(manifest.read_text(encoding="utf-8")).get("http", {}).get("base", "")
223
+ print(f" 清单地址已更新:{base}")
224
+ print("\n下一步:mcai doctor --url " + info["new"] + " [--token <令牌>]")
225
+ return 0
226
+
227
+
228
+ def cmd_doctor(args: argparse.Namespace) -> int:
229
+ from .deploy import doctor
230
+ token = (getattr(args, "token", "") or os.environ.get("MCAI_TOKEN", "")).strip()
231
+ checks = doctor(args.url, token=token)
232
+ print(f"体检目标:{args.url}\n")
233
+ failed = 0
234
+ for c in checks:
235
+ mark = "✅" if c["ok"] else "❌"
236
+ print(f"{mark} {c['name']:<10} {c['detail']}")
237
+ if not c["ok"]:
238
+ failed += 1
239
+ if c.get("hint"):
240
+ print(f" ↳ {c['hint']}")
241
+ print()
242
+ if failed:
243
+ print(f"共 {failed} 项未通过。")
244
+ return 1
245
+ print("全部通过。")
246
+ return 0
247
+
248
+
249
+ def cmd_report(args: argparse.Namespace) -> int:
250
+ kb = pipeline.load_kb(_home(args))
251
+ stats = kb["meta"]["stats"]
252
+ print(f"构建时间:{kb['meta']['built_at']}")
253
+ print(f"概念 {stats['concepts']} · 问答 {stats['faqs']} · 关系 {stats['relations']} "
254
+ f"· 引证 {stats.get('evidence', 0)} · 文档 {stats['documents']} · 段落 {stats['blocks']} "
255
+ f"· 字数 {stats['characters']:,}")
256
+ print("\n文档类型:")
257
+ for name, count in stats.get("by_kind", {}).items():
258
+ print(f" {name:<12} {count:>3}")
259
+ print("\n分类:")
260
+ for name, count in stats["categories"].items():
261
+ print(f" {name:<12} {count:>3}")
262
+ print("\n状态:")
263
+ for name, count in stats["status"].items():
264
+ print(f" {name:<12} {count:>3}")
265
+ conflicts = [c for c in kb["concepts"] if c["status"] == "conflict"]
266
+ if conflicts:
267
+ print("\n冲突条目:")
268
+ for c in conflicts:
269
+ print(f" - {c['term']}({len(c['variants'])} 个版本)")
270
+ cov = stats["coverage"]
271
+ print(f"\n词表覆盖:{cov['covered']}/{cov['expected']}")
272
+ if cov["missing"]:
273
+ print(" 缺失:" + "、".join(cov["missing"]))
274
+ return 0
275
+
276
+
277
+ def cmd_disambig(args: argparse.Namespace) -> int:
278
+ from . import knowledge
279
+ kb = knowledge.load(_home(args))
280
+ camp = kb.meta.get("stats", {}).get("disambiguation") or {}
281
+ print(f"阶段性消歧:{'开启' if camp.get('enabled') else '关闭'}")
282
+ print(f" 阶段名:{camp.get('campaign', '')}")
283
+ print(f" 复核期:{camp.get('review_after', '')}")
284
+ print(f" 覆盖术语:{camp.get('items', 0)} 条")
285
+ print(f" 相关问答:{kb.meta.get('stats', {}).get('campaign_faqs', 0)} 组")
286
+ print("\n说明:" + camp.get("note", ""))
287
+ print("\n开关:编辑 config/disambiguation.json 的 campaign.enabled,然后 mcai build")
288
+ return 0
289
+
290
+
291
+ def cmd_gaps(args: argparse.Namespace) -> int:
292
+ from .telemetry import for_root
293
+ rows = for_root(_home(args)).gaps(limit=args.limit)
294
+ if not rows:
295
+ print("暂无未命中的问题。")
296
+ return 0
297
+ print(f"未命中问题({len(rows)} 条),可作为人工补词输入:")
298
+ for r in rows:
299
+ print(f" [{r['count']}次] {r['query']} (最近 {r['last_seen']})")
300
+ return 0
301
+
302
+
303
+ def cmd_stats(args: argparse.Namespace) -> int:
304
+ from .telemetry import for_root
305
+ s = for_root(_home(args)).stats()
306
+ origin = s.get("by_origin") or {}
307
+ print(f"累计 {s['total']} 次:业务问答 {s.get('business', 0)} · 健康探测 {s.get('probes', 0)}")
308
+ print(f"批量问答:{s.get('batch_calls', 0)} 次调用 / {s.get('batch_answers', 0)} 个问题 · "
309
+ f"命中率 {round((s.get('hit_rate') or 0) * 100)}%")
310
+ print(f"来源:本机 {origin.get('local', 0)} · 外部 {origin.get('external', 0)} · 未标记 {origin.get('unknown', 0)}")
311
+ kinds = s.get("by_client_kind") or {}
312
+ print(f"客户端:自检 {kinds.get('self', 0)} · 命令行 {kinds.get('shell', 0)} · 浏览器 {kinds.get('browser', 0)} · MCP {kinds.get('mcp', 0)}")
313
+ print(f"渠道:{s['by_channel']}")
314
+ print(f"类型:{s['by_type']}")
315
+ print("最近 14 天:")
316
+ for day, n in s["by_day"].items():
317
+ bar = "█" * min(n, 40)
318
+ print(f" {day} {n:>3} {bar}")
319
+ if s["top_questions"]:
320
+ print("高频问题:")
321
+ for item in s["top_questions"][:8]:
322
+ print(f" [{item['count']}] {item['question']}")
323
+ return 0
324
+
325
+
326
+ def cmd_export(args: argparse.Namespace) -> int:
327
+ from . import dataset
328
+ from .corpus import load_corpus
329
+ from .disambig import load as load_disambig
330
+ from .extract import build as build_kb
331
+
332
+ home = _home(args)
333
+ corpus = load_corpus(home / "config" / "corpus.json")
334
+ curated = load_config(home / "config" / "curated.json")
335
+ overrides_path = home / "config" / "overrides.json"
336
+ overrides = load_config(overrides_path) if overrides_path.exists() else {}
337
+ disambig = load_disambig(home / "config" / "disambiguation.json")
338
+ kb = build_kb(corpus, curated, overrides, disambig)
339
+ result = dataset.export(kb, args.out or home / "export")
340
+ print(json.dumps(result, ensure_ascii=False, indent=2))
341
+ return 0
342
+
343
+
344
+ def cmd_probe(args: argparse.Namespace) -> int:
345
+ from . import knowledge, probe
346
+ from .llm import LLM
347
+
348
+ home = _home(args)
349
+ kb = knowledge.load(home)
350
+ llm = LLM(home / "config" / "llm.json")
351
+ if not llm.available:
352
+ print("❌ 未找到 API 密钥:请检查 config/llm.json 的 key_path,或设置 DEEPSEEK_API_KEY")
353
+ return 1
354
+ questions = probe.build_questions(kb, limit=args.limit)
355
+ if not questions:
356
+ print("❌ 没有生成出题目,请检查知识库")
357
+ return 1
358
+ print(f"抽检 {len(questions)} 道题 × 2 种条件(baseline / grounded)…")
359
+
360
+ def progress(i: int, total: int, label: str) -> None:
361
+ print(f" [{i}/{total}] {label}")
362
+
363
+ result = probe.run(llm, kb, questions, progress=progress)
364
+ report = probe.write_report(result, home / "reports")
365
+ s = report["summary"]
366
+ print("\n结果:")
367
+ print(f" 答案对齐率 baseline {s['baseline']['alignment_rate']:.0%} → "
368
+ f"grounded {s['grounded']['alignment_rate']:.0%}({s['uplift']['alignment_rate']:+.0%})")
369
+ print(f" 答案覆盖度 baseline {s['baseline']['coverage_mean']:.2f} → "
370
+ f"grounded {s['grounded']['coverage_mean']:.2f}({s['uplift']['coverage']:+.2f})")
371
+ print(f" 学说术语数 baseline {s['baseline']['kb_terms_mean']:.1f} → "
372
+ f"grounded {s['grounded']['kb_terms_mean']:.1f}({s['uplift']['kb_terms']:+.1f})")
373
+ print(f" 报告:{report['markdown']}")
374
+ return 0
375
+
376
+
377
+ def cmd_factory(args: argparse.Namespace) -> int:
378
+ from . import factory, knowledge
379
+ from .llm import LLM
380
+
381
+ home = _home(args)
382
+ kb = knowledge.load(home)
383
+ names = [n.strip() for n in (args.concepts or "").split(",") if n.strip()]
384
+ if names:
385
+ concepts = []
386
+ for name in names:
387
+ c = kb.resolve(name)
388
+ if not c:
389
+ print(f"⚠️ 未找到术语:{name}")
390
+ continue
391
+ concepts.append(c)
392
+ else:
393
+ order = ["元意识", "观测创造实在", "序生智能体"]
394
+ concepts = [kb.by_term[t] for t in order if t in kb.by_term][: args.top]
395
+ if not concepts:
396
+ print("❌ 没有可用的概念")
397
+ return 1
398
+
399
+ formats = list(factory.FORMATS) if args.formats == "all" else [
400
+ f.strip() for f in args.formats.split(",") if f.strip() in factory.FORMATS
401
+ ]
402
+ if not formats:
403
+ print("❌ 未知的格式,可选:" + "、".join(factory.FORMATS))
404
+ return 1
405
+
406
+ llm = LLM(home / "config" / "llm.json")
407
+ offline = args.offline or not llm.available
408
+ if offline and not args.offline:
409
+ print("⚠️ 未找到 API 密钥,自动切换到离线模板模式")
410
+
411
+ print(f"生成 {len(concepts)} 个概念 × {len(formats)} 种格式 → {home / 'content'}")
412
+
413
+ def progress(i: int, total: int, label: str) -> None:
414
+ print(f" [{i}/{total}] {label}")
415
+
416
+ result = factory.generate_batch(
417
+ llm if not offline else None, kb, concepts, formats,
418
+ home / "content", force=args.force, offline=offline, progress=progress,
419
+ )
420
+ print(f"\n完成:新建 {len(result['created'])},跳过 {len(result['skipped'])},失败 {len(result['failed'])}")
421
+ for item in result["created"]:
422
+ flag = "" if item["status"] == "draft" else "(需复核:" + "、".join(item["issues"]) + ")"
423
+ print(f" · {item['concept']} / {item['format']} {flag}")
424
+ for item in result["failed"]:
425
+ print(f" ✗ {item['concept']} / {item['format']}:{item['error']}")
426
+ return 0 if not result["failed"] else 2
427
+
428
+
429
+ def build_parser() -> argparse.ArgumentParser:
430
+ common = argparse.ArgumentParser(add_help=False)
431
+ common.add_argument("--home", default=None, help="工作区目录(默认:MCAI_HOME 或当前目录)")
432
+
433
+ parser = argparse.ArgumentParser(prog="mcai", description="元意识知识资产引擎")
434
+ parser.add_argument("--version", action="version", version="mcai-knowledge 0.1.0")
435
+ sub = parser.add_subparsers(dest="command")
436
+
437
+ p_init = sub.add_parser("init", parents=[common], help="初始化一个工作区(复制配置模板)")
438
+ p_init.add_argument("path", nargs="?", default=".", help="目标目录,默认当前目录")
439
+ p_init.add_argument("--force", action="store_true", help="覆盖已有配置")
440
+ p_init.set_defaults(func=cmd_init)
441
+
442
+ p_build = sub.add_parser("build", parents=[common], help="从语料重建知识库与站点")
443
+ p_build.add_argument("--serve", action="store_true", help="构建后启动本地服务")
444
+ p_build.add_argument("--port", type=int, default=8787)
445
+ p_build.set_defaults(func=cmd_build)
446
+
447
+ p_serve = sub.add_parser("serve", parents=[common], help="启动本地 Web 应用与 HTTP API")
448
+ p_serve.add_argument("--port", type=int, default=8787)
449
+ p_serve.add_argument("--host", default="127.0.0.1", help="监听地址;对外暴露用 0.0.0.0")
450
+ p_serve.add_argument("--token", default="", help="Bearer 令牌;也可用环境变量 MCAI_TOKEN")
451
+ p_serve.add_argument("--rate-limit", type=int, default=60, help="每 IP 每分钟请求上限")
452
+ p_serve.add_argument("--autopilot", type=int, default=0, metavar="秒",
453
+ help="在服务内启用自动驾驶:每 N 秒检查语料并自检(0=关闭)")
454
+ p_serve.set_defaults(func=cmd_serve)
455
+
456
+ p_api = sub.add_parser("api", parents=[common], help="只启动 HTTP API(同一套服务)")
457
+ p_api.add_argument("--port", type=int, default=8788)
458
+ p_api.add_argument("--host", default="127.0.0.1")
459
+ p_api.add_argument("--token", default="")
460
+ p_api.add_argument("--rate-limit", type=int, default=60)
461
+ p_api.add_argument("--autopilot", type=int, default=0, metavar="秒")
462
+ p_api.set_defaults(func=cmd_api)
463
+
464
+ p_mcp = sub.add_parser("mcp", parents=[common], help="启动 MCP 服务器(stdio)")
465
+ p_mcp.set_defaults(func=cmd_mcp)
466
+
467
+ p_auto = sub.add_parser("autopilot", parents=[common],
468
+ help="自动驾驶:检查语料、自动重建、健康自检")
469
+ p_auto.add_argument("--once", action="store_true", help="只跑一轮(适合计划任务)")
470
+ p_auto.add_argument("--interval", type=int, default=60, help="常驻模式的检查间隔秒数")
471
+ p_auto.add_argument("--port", type=int, default=8787)
472
+ p_auto.add_argument("--no-server", action="store_true", help="不自动拉起服务")
473
+ p_auto.set_defaults(func=cmd_autopilot)
474
+
475
+ p_quality = sub.add_parser("quality", parents=[common], help="质量巡检:定义、关系、死链、JSON-LD")
476
+ p_quality.set_defaults(func=cmd_quality)
477
+
478
+ p_selftest = sub.add_parser("selftest", parents=[common], help="接口自测:MCP 工具与关键问答")
479
+ p_selftest.set_defaults(func=cmd_selftest)
480
+
481
+ p_snap = sub.add_parser("snapshot", parents=[common], help="生成一份本地快照(zip,自动轮转)")
482
+ p_snap.add_argument("-m", "--note", default="", help="备注,例如「接入新语料前」")
483
+ p_snap.add_argument("--keep", type=int, default=8, help="保留最近几份,默认 8")
484
+ p_snap.add_argument("--no-corpus", action="store_true", help="不打包语料文件(包更小)")
485
+ p_snap.set_defaults(func=cmd_snapshot)
486
+
487
+ p_snaps = sub.add_parser("snapshots", parents=[common], help="列出已有快照")
488
+ p_snaps.add_argument("--limit", type=int, default=20)
489
+ p_snaps.set_defaults(func=cmd_snapshots)
490
+
491
+ p_restore = sub.add_parser("restore", help="把快照恢复到指定目录")
492
+ p_restore.add_argument("archive", help="快照 zip 的路径")
493
+ p_restore.add_argument("--to", required=True, help="恢复目标目录(建议新目录)")
494
+ p_restore.add_argument("--force", action="store_true", help="目标非空时覆盖")
495
+ p_restore.set_defaults(func=cmd_restore)
496
+
497
+ p_dash = sub.add_parser("dashboard", parents=[common], help="生成本地观测台页面")
498
+ p_dash.set_defaults(func=cmd_dashboard)
499
+
500
+ p_status = sub.add_parser("status", parents=[common], help="查看自动驾驶写入的状态")
501
+ p_status.set_defaults(func=cmd_status)
502
+
503
+ p_token = sub.add_parser("token", help="生成一个 Bearer 令牌")
504
+ p_token.set_defaults(func=cmd_token)
505
+
506
+ p_seturl = sub.add_parser("set-url", parents=[common], help="设置对外地址并重建(更新清单里的链接)")
507
+ p_seturl.add_argument("url", help="如 https://xxx.trycloudflare.com")
508
+ p_seturl.add_argument("--no-build", action="store_true", help="只改配置,不立即重建")
509
+ p_seturl.set_defaults(func=cmd_set_url)
510
+
511
+ p_doctor = sub.add_parser("doctor", help="对已部署实例做远程体检")
512
+ p_doctor.add_argument("--url", required=True, help="如 https://xxx.trycloudflare.com")
513
+ p_doctor.add_argument("--token", default="", help="Bearer 令牌;也可用 MCAI_TOKEN")
514
+ p_doctor.set_defaults(func=cmd_doctor)
515
+
516
+ p_report = sub.add_parser("report", parents=[common], help="知识库体检报告")
517
+ p_report.set_defaults(func=cmd_report)
518
+
519
+ p_dis = sub.add_parser("disambig", parents=[common], help="阶段性消歧状态")
520
+ p_dis.set_defaults(func=cmd_disambig)
521
+
522
+ p_stats = sub.add_parser("stats", parents=[common], help="调用统计")
523
+ p_stats.set_defaults(func=cmd_stats)
524
+
525
+ p_gaps = sub.add_parser("gaps", parents=[common], help="未命中的问题(人工补词输入)")
526
+ p_gaps.add_argument("--limit", type=int, default=20)
527
+ p_gaps.set_defaults(func=cmd_gaps)
528
+
529
+ p_probe = sub.add_parser("probe", parents=[common], help="抽检模型的自发知识与注入增益")
530
+ p_probe.add_argument("--limit", type=int, default=12)
531
+ p_probe.set_defaults(func=cmd_probe)
532
+
533
+ p_factory = sub.add_parser("factory", parents=[common], help="内容工厂(可选配件)")
534
+ p_factory.add_argument("--concepts", type=str, default="")
535
+ p_factory.add_argument("--top", type=int, default=3)
536
+ p_factory.add_argument("--formats", type=str, default="all")
537
+ p_factory.add_argument("--offline", action="store_true")
538
+ p_factory.add_argument("--force", action="store_true")
539
+ p_factory.set_defaults(func=cmd_factory)
540
+
541
+ p_export = sub.add_parser("export", parents=[common], help="仅导出数据集")
542
+ p_export.add_argument("--out", type=str, default="")
543
+ p_export.set_defaults(func=cmd_export)
544
+
545
+ return parser
546
+
547
+
548
+ def main(argv: list[str] | None = None) -> int:
549
+ parser = build_parser()
550
+ args = parser.parse_args(argv)
551
+ if not getattr(args, "func", None):
552
+ parser.print_help()
553
+ return 0
554
+ return args.func(args)
555
+
556
+
557
+ if __name__ == "__main__":
558
+ sys.exit(main())