|
|
@@ -831,6 +831,100 @@ def cmd_pull(args):
|
|
|
return 0
|
|
|
|
|
|
|
|
|
+# ---------------------------------------------------------------- stash
|
|
|
+
|
|
|
+SOURCE_LABEL = {
|
|
|
+ "gzh": "微信公众号", "ima": "IMA 知识库", "web": "网页",
|
|
|
+ "feishu": "飞书文档", "yuque": "语雀", "notion": "Notion",
|
|
|
+ "pdf": "PDF 文件", "doc": "文档", "video": "视频转写",
|
|
|
+ "audio": "音频转写", "chat": "聊天记录", "other": "其他",
|
|
|
+}
|
|
|
+# 外部来源默认证据等级的建议(真正定级由 kb-ingest 按内容判断)
|
|
|
+SOURCE_HINT = {
|
|
|
+ "gzh": "公众号属二手解读,默认 C 级。文章里若有可核验的一手数据且标了出处,可升 B",
|
|
|
+ "ima": "IMA 里的笔记是别人的加工品,默认 C 级。笔记里给出原始出处链接才可升 B",
|
|
|
+ "web": "按发布方定:官方一手渠道可到 A,媒体转述最多 B",
|
|
|
+ "feishu": "内部文档:会议纪要/访谈记录属一手,B 级;总结稿属二手,C 级",
|
|
|
+ "yuque": "同内部文档,按是否一手记录判定",
|
|
|
+ "notion": "同内部文档,按是否一手记录判定",
|
|
|
+ "video": "转写文本,按说话人身份判定;讲师经验 C 级,客户原话 B 级",
|
|
|
+ "audio": "转写文本,按说话人身份判定",
|
|
|
+ "chat": "聊天记录属一手,但碎片化,B 级",
|
|
|
+ "other": "按内容性质自行判定",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+def cmd_stash(args):
|
|
|
+ """把外部抓来的内容规范落盘到原材料目录。
|
|
|
+
|
|
|
+ 外部来源(公众号、IMA、网页)不能直接进知识库 —— 页面回滚靠 sources 里的
|
|
|
+ 相对路径,内容必须先是磁盘上的一个文件。这个命令负责把「抓来的东西」变成
|
|
|
+ 「一个带来源信息的原材料文件」。
|
|
|
+ """
|
|
|
+ body = ""
|
|
|
+ if args.body_file:
|
|
|
+ bp = Path(args.body_file)
|
|
|
+ if not bp.exists():
|
|
|
+ print(f"[ERR] 正文文件不存在:{bp}")
|
|
|
+ return 1
|
|
|
+ body = bp.read_text(encoding="utf-8", errors="replace")
|
|
|
+ elif not sys.stdin.isatty():
|
|
|
+ body = sys.stdin.read()
|
|
|
+ body = body.strip()
|
|
|
+ if not body:
|
|
|
+ print("[ERR] 没有正文。用 --body-file <文件> 或通过管道传入:")
|
|
|
+ print(" cat article.md | python kb.py stash --title \"...\" --type gzh")
|
|
|
+ return 1
|
|
|
+
|
|
|
+ title = (args.title or "").strip()
|
|
|
+ if not title: # 没给标题就从正文第一个 H1 里取
|
|
|
+ m = re.search(r"^#\s+(.+)$", body, re.M)
|
|
|
+ title = m.group(1).strip() if m else "未命名"
|
|
|
+ # 正文开头已有同名 H1 就去掉,避免重复
|
|
|
+ head = f"# {title}"
|
|
|
+ if body.startswith(head):
|
|
|
+ rest = body[len(head):]
|
|
|
+ if rest.startswith("\n") or not rest.strip():
|
|
|
+ body = rest.lstrip("\n").strip()
|
|
|
+
|
|
|
+ date = args.date or datetime.now(CST).strftime("%Y-%m-%d")
|
|
|
+ typ = args.type
|
|
|
+ if typ not in SOURCE_LABEL:
|
|
|
+ print(f"[!] 未知类型 {typ},按 other 处理。可选:{', '.join(SOURCE_LABEL)}")
|
|
|
+ typ = "other"
|
|
|
+
|
|
|
+ dest = RAW / (args.dest or "_inbox")
|
|
|
+ dest.mkdir(parents=True, exist_ok=True)
|
|
|
+ slug = re.sub(r'[\\/:*?"<>|、,。!?\s]+', "-", title).strip("-")[:48] or "untitled"
|
|
|
+ p = dest / f"{date}-{typ}-{slug}.md"
|
|
|
+ n = 2
|
|
|
+ while p.exists():
|
|
|
+ p = dest / f"{date}-{typ}-{slug}-{n}.md"
|
|
|
+ n += 1
|
|
|
+
|
|
|
+ meta = [f"<!-- 来源: {SOURCE_LABEL[typ]}{'「' + args.author + '」' if args.author else ''} -->"]
|
|
|
+ if args.author:
|
|
|
+ meta.append(f"<!-- 作者: {args.author} -->")
|
|
|
+ if args.url:
|
|
|
+ meta.append(f"<!-- 原链接: {args.url} -->")
|
|
|
+ if args.archive:
|
|
|
+ meta.append(f"<!-- 存档: {args.archive} -->")
|
|
|
+ meta.append(f"<!-- 抓取时间: {datetime.now(CST).strftime('%Y-%m-%d %H:%M')} -->")
|
|
|
+ meta.append(f"<!-- 证据提示: {SOURCE_HINT.get(typ, SOURCE_HINT['other'])} -->")
|
|
|
+
|
|
|
+ p.write_text("\n".join(meta) + f"\n\n# {title}\n\n{body}\n", encoding="utf-8")
|
|
|
+
|
|
|
+ rel = p.relative_to(RAW).as_posix()
|
|
|
+ print(f"[OK] 已落盘:{rel}({len(body)} 字)")
|
|
|
+ if not args.url:
|
|
|
+ print(" ⚠️ 没记原链接。将来引用这句话时,无法回溯到原文出处。")
|
|
|
+ if typ in ("gzh", "web") and not args.archive:
|
|
|
+ print(f" ⚠️ 没做网页存档。公众号和网页会失效,建议先存一份:")
|
|
|
+ print(f" https://web.archive.org/save/{args.url or '<原链接>'}")
|
|
|
+ print("\n 下一步:跑一次同步把它整理进知识库(说「同步知识库」即可)")
|
|
|
+ return 0
|
|
|
+
|
|
|
+
|
|
|
# ---------------------------------------------------------------- repair
|
|
|
|
|
|
def cmd_repair(args):
|
|
|
@@ -951,6 +1045,17 @@ def main():
|
|
|
|
|
|
sub.add_parser("repair", help="从磁盘和页面反推 manifest(git 冲突后用)")
|
|
|
|
|
|
+ st = sub.add_parser("stash", help="把外部抓来的内容规范落盘到原材料目录")
|
|
|
+ st.add_argument("--title", default="")
|
|
|
+ st.add_argument("--url", default="")
|
|
|
+ st.add_argument("--type", default="other",
|
|
|
+ help="gzh/ima/web/feishu/yuque/notion/pdf/doc/video/audio/chat/other")
|
|
|
+ st.add_argument("--author", default="")
|
|
|
+ st.add_argument("--archive", default="", help="网页存档链接")
|
|
|
+ st.add_argument("--body-file", dest="body_file", default=None)
|
|
|
+ st.add_argument("--date", default=None)
|
|
|
+ st.add_argument("--dest", default="_inbox", help="落到哪个子目录,默认 _inbox")
|
|
|
+
|
|
|
pl = sub.add_parser("pull", help="从远端拉最新并报告原材料变化")
|
|
|
pl.add_argument("--remote", default=None)
|
|
|
pl.add_argument("--force", action="store_true", help="工作区有改动也拉")
|
|
|
@@ -959,7 +1064,7 @@ def main():
|
|
|
fn = {"status": cmd_status, "guide": cmd_guide, "scan": cmd_scan, "mark": cmd_mark,
|
|
|
"retract": cmd_retract, "move": cmd_move, "graph": cmd_graph,
|
|
|
"lint": cmd_lint, "check": cmd_check, "repair": cmd_repair,
|
|
|
- "pull": cmd_pull}.get(args.cmd)
|
|
|
+ "pull": cmd_pull, "stash": cmd_stash}.get(args.cmd)
|
|
|
if not fn:
|
|
|
ap.print_help()
|
|
|
return 0
|