Prechádzať zdrojové kódy

feat: 远端优先工作流 + kb.py pull

kb.py pull:先对齐远端再整理。报出原材料新增/修改/删除/移动,工作区脏时拒绝拉取并给出三个选项。
空目录补 .gitkeep —— git 不跟踪空目录,clone 后 00-原材料 五个子目录会整个消失。
kb-sync 加 Step 0(先 pull,避免整理别人已改过的东西)与 Step 8(收工推送)。
GIT.md 新增「远端优先」章节:为什么读写型知识库不能照抄只读的临时副本模式。
guide / course-factory 同步。
shencp 18 hodín pred
rodič
commit
ed4c28d086

+ 0 - 0
00-原材料/01-行业资料/.gitkeep


+ 0 - 0
00-原材料/02-客户与需求/.gitkeep


+ 0 - 0
00-原材料/03-课程素材/.gitkeep


+ 0 - 0
00-原材料/04-竞品与参考/.gitkeep


+ 0 - 0
00-原材料/_inbox/.gitkeep


+ 0 - 0
10-知识库/_archive/.gitkeep


+ 0 - 0
20-课程产出/_模板/.gitkeep


+ 34 - 1
30-技能/kb-sync/SKILL.md

@@ -21,6 +21,24 @@ description: |-
 
 ## 执行流程
 
+### Step 0 · 先对齐远端(配置了 remote 才做)
+
+```bash
+python 90-工具/kb.py pull
+```
+
+**顺序不能反。** 如果先整理本地、再拉远端,很可能整理的是别人已经改过的东西,白干。
+
+三种结果:
+
+| 输出 | 含义 | 动作 |
+|---|---|---|
+| `没有配置远端` | 纯本地库 | 跳过这步,直接 Step 1 |
+| `已是最新` | 没有新提交 | 直接 Step 1 |
+| `拉到 N 个新提交` | 别人推了新料 | 记下它列出的文件清单,Step 5 会逐个处理 |
+
+如果它报「工作区有未提交改动」,**先提交或 stash 再拉**。别用 `--force`,那会把未提交的整理成果 rebase 丢。
+
 ### Step 1 · 看现状
 
 ```bash
@@ -239,7 +257,22 @@ lint 有问题就当场修,别攒着:
 
 ---
 
-## Step 8 · 汇报
+## Step 8 · 提交并推送(配置了 remote 才做)
+
+整理完不推,等于白干:别人拉不到,你自己换台机器也接不上。
+
+```bash
+python 90-工具/kb.py check          # 结构 + 内容都过一遍
+git add -A
+git commit -m "sync: 入库 3 份行业报告,派生 9 页"
+git push
+```
+
+提交信息用 `sync:` / `retract:` / `fix:` 前缀,规范见 `GIT.md`。
+
+---
+
+## Step 9 · 汇报
 
 ```md
 同步完成

+ 117 - 2
90-工具/kb.py

@@ -678,12 +678,13 @@ def cmd_guide(args):
   3. 补料不许编,取材必标出处 —— 这是两条方向相反的规则,见 AGENTS.md
 
 【该走哪条路】
+  开工第一件事                → kb.py pull             (对齐远端,报出别人推了什么)
   丢进了新资料 / 删了源文件    → 30-技能/kb-sync       (先 scan 再分流)
   查点东西                    → 30-技能/kb-query      (好答案记得沉淀回库)
   要做一门课                  → 30-技能/course-outline → course-slides
   库看着有点乱                → kb.py check           (结构 + 内容一起过)
   改页面格式或加规则          → 10-知识库/_meta/约定.md(改完登记起因)
-  改完要留档                  → GIT.md                (一次同步一个 commit)
+  收工                        → GIT.md 的提交规范      (不推等于白干)
   manifest 被 git 搞冲突了    → kb.py repair          (从磁盘和页面反推)
 
 【格式硬约束】(细节见 约定.md)
@@ -721,6 +722,115 @@ def cmd_guide(args):
     print("=" * 60)
 
 
+# ---------------------------------------------------------------- pull
+
+def git(*args):
+    """在仓库根跑 git。Windows 下 git 输出可能是 GBK,两种都试。"""
+    r = subprocess.run(["git", *args], cwd=str(ROOT), capture_output=True,
+                       text=True, encoding="utf-8", errors="replace")
+    if r.returncode != 0 and r.stderr and "codec" in r.stderr.lower():
+        r = subprocess.run(["git", *args], cwd=str(ROOT), capture_output=True,
+                           text=True, encoding="gbk", errors="replace")
+    return r
+
+
+def cmd_pull(args):
+    """远端优先:先拉最新,再告诉你有多少新料要整理。
+
+    设计取向:这个库是要写入的,所以不能像只读知识库那样"拉临时副本用完就扔"。
+    正确做法是本地留全量,每次开工先 pull —— pull 只传增量,比全量下载快得多。
+    """
+    if git("rev-parse", "--is-inside-work-tree").returncode != 0:
+        print("[ERR] 这里不是 git 仓库,无法从远端拉取")
+        print("     如果是新建的库,先 git init 并配置 origin")
+        return 1
+
+    remotes = git("remote").stdout.split()
+    if not remotes:
+        print("[!] 没有配置远端,当前只有本地版本控制")
+        print("    配置:git remote add origin <你的本地 git 服务地址>")
+        print("    然后照常跑 kb.py scan")
+        return 0
+
+    remote = args.remote or remotes[0]
+    branch = git("rev-parse", "--abbrev-ref", "HEAD").stdout.strip() or "main"
+
+    # 工作区不干净就别拉,免得跟 rebase 打架
+    dirty = [x for x in git("status", "--porcelain").stdout.splitlines() if x.strip()]
+    if dirty and not args.force:
+        print(f"[!] 工作区有 {len(dirty)} 处未提交改动,先处理再拉:")
+        for d in dirty[:8]:
+            print(f"    {d}")
+        if len(dirty) > 8:
+            print(f"    ... 还有 {len(dirty)-8} 处")
+        print("\n  提交:git add -A && git commit -m \"sync: ...\"")
+        print("  或暂存:git stash")
+        print(f"  或强制拉(会 rebase 上去):kb.py pull --force")
+        return 1
+
+    before = git("rev-parse", "HEAD").stdout.strip()
+    print(f"从 {remote}/{branch} 拉取…")
+    r = git("pull", "--rebase", remote, branch)
+    out = (r.stdout + r.stderr).strip()
+    if r.returncode != 0:
+        print("[ERR] 拉取失败:")
+        for line in out.splitlines()[:12]:
+            print("   ", line)
+        if "conflict" in out.lower() or "CONFLICT" in out:
+            print("\n  有冲突。解法见 GIT.md「冲突怎么解」:")
+            print("    manifest.json      → git checkout --ours 后跑 kb.py repair")
+            print("    */_index.md        → git checkout --theirs 后跑 kb.py graph")
+        return 1
+
+    after = git("rev-parse", "HEAD").stdout.strip()
+    if before == after:
+        print("已是最新,没有新提交。")
+        return cmd_scan(argparse.Namespace(json=False))
+
+    # 报远端这次带来了什么
+    diff = git("diff", "--name-status", "-M", f"{before}..{after}", "--", "00-原材料/").stdout
+    add, mod, dele, mov = [], [], [], []
+    for line in diff.splitlines():
+        parts = line.split("\t")
+        if len(parts) < 2:
+            continue
+        st, path = parts[0], parts[-1]
+        if st.startswith("A"):
+            add.append(path)
+        elif st.startswith("M"):
+            mod.append(path)
+        elif st.startswith("D"):
+            dele.append(path)
+        elif st.startswith("R"):
+            mov.append((parts[1], parts[2]))
+
+    n_commits = git("rev-list", "--count", f"{before}..{after}").stdout.strip()
+    print(f"拉到 {n_commits} 个新提交。原材料变化:"
+          f"新增 {len(add)} · 修改 {len(mod)} · 删除 {len(dele)} · 移动 {len(mov)}")
+    for x in add:
+        print(f"  [NEW]     {x}")
+    for x in mod:
+        print(f"  [CHANGED] {x}")
+    for x in dele:
+        print(f"  [DELETED] {x}")
+    for a, b in mov:
+        print(f"  [MOVED]   {a} -> {b}")
+
+    if any(x.lower().endswith((".pdf", ".mp4", ".mov", ".zip")) for x in add):
+        print("\n  提示:新增了可能很大的文件。如果仓库用了 git-lfs,")
+        print("        确认 lfs 对象已拉下来(git lfs pull),否则整理时读不到内容。")
+
+    # 页面上可能也带了变化(别人整理的结果)
+    page_diff = git("diff", "--stat", f"{before}..{after}", "--", "10-知识库/").stdout
+    if page_diff.strip():
+        print(f"\n  知识库页面也有更新:")
+        for line in page_diff.strip().splitlines()[-6:]:
+            print(f"    {line.strip()}")
+
+    print("\n  下一步:让模型按 kb-sync 的流程处理上面这些变化。")
+    return 0
+
+
 # ---------------------------------------------------------------- repair
 
 def cmd_repair(args):
@@ -841,10 +951,15 @@ def main():
 
     sub.add_parser("repair", help="从磁盘和页面反推 manifest(git 冲突后用)")
 
+    pl = sub.add_parser("pull", help="从远端拉最新并报告原材料变化")
+    pl.add_argument("--remote", default=None)
+    pl.add_argument("--force", action="store_true", help="工作区有改动也拉")
+
     args = ap.parse_args()
     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}.get(args.cmd)
+          "lint": cmd_lint, "check": cmd_check, "repair": cmd_repair,
+          "pull": cmd_pull}.get(args.cmd)
     if not fn:
         ap.print_help()
         return 0

+ 76 - 9
GIT.md

@@ -153,25 +153,92 @@ git diff HEAD~20 --stat
 
 ---
 
-## 要不要推到远端
+## 远端优先:每次开工先拉
 
-**不要推到公开 GitHub。** 这个库里有客户名称、报价、内部方法论,属于公司资产。
+git 服务在本机或内网,不存在外泄风险,所以这个库可以、也应该**以远端为准**。
 
-需要多人协作时的选项:
+### 但有个前提要说清
 
-| 方案 | 适合 |
+公开的只读知识库(比如那本人生指南)可以「拉一份临时副本 → 读完扔掉」,因为它只读。
+
+**这个库要写入。** 拉下来的副本改完往哪推?所以不能照抄那个模式。正确的做法是:
+
+> 本地保留完整工作区,但**每次都从远端拉最新**,改完推回去。
+
+「远端为准」靠的不是「本地不存」,而是**每次开工先把本地对齐到远端**。
+
+### 一条命令
+
+```bash
+python 90-工具/kb.py pull
+```
+
+它做四件事:
+
+1. 检查是不是 git 仓库、有没有配远端
+2. **工作区有未提交改动就拒绝拉取** —— 避免和 rebase 打架,并列出脏文件
+3. `git pull --rebase` 拉最新
+4. 对比拉取前后的提交,**报出原材料新增/修改/删除/移动各几个,具体是哪些文件**
+
+输出长这样:
+
+```
+从 origin/main 拉取…
+拉到 1 个新提交。原材料变化:新增 2 · 修改 0 · 删除 0 · 移动 0
+  [NEW]     00-原材料/01-行业资料/远端新增报告.md
+  [NEW]     00-原材料/03-课程素材/远端新案例.md
+
+  下一步:让模型按 kb-sync 的流程处理上面这些变化。
+```
+
+拉完之后**不需要额外的检测逻辑**:文件真的落地了,`kb.py scan` 的 sha256 比对自然能发现它们。实测这比让 git 自己算 diff 更可靠 —— 它还能发现没提交的本地改动。
+
+### 为什么这样最省时间
+
+`git pull` 只传增量。唯一的全量传输是第一次 `git clone`,之后每次都是几百 KB 级别。
+
+反过来,如果为了省磁盘只检出需要的部分(sparse-checkout),`kb.py scan` 就扫不到全量原材料,指纹比对直接失效 —— 省下的空间远不值得。
+
+### 按仓库体积选策略
+
+| 仓库体积 | 做法 |
 |---|---|
-| 公司内网 GitLab / Gitee 私有仓库 | 正式团队协作 |
-| 局域网共享盘 + 各自本地仓库 | 三两个人,临时 |
-| 一人维护,其他人只读 | 最常见,用 `git pull` 拿最新 |
+| < 500 MB | **全量 + 每次 pull**(默认,就是现在这套) |
+| 500 MB – 5 GB | 全量 + 大文件走 git-lfs(lfs 对象按需下载,工作区里是几百字节的指针) |
+| > 5 GB 或含大量视频 | 原材料单独开一个仓库,主库只放知识库与技能 |
 
-远端地址配好后:
+绝大多数做课资料在第一档。
+
+### 日常节奏
+
+```
+早上开工   python 90-工具/kb.py pull      # 拿到别人昨天整理的结果
+白天       丢资料 → 说「同步」
+晚上收工   git add -A && git commit && git push
+```
+
+**收工不推,等于白干。** 第二天别人 pull 不到,你自己换台机器也接不上。
+
+---
+
+## 首次配置远端
+
+```bash
+git remote add origin <你的本地 git 服务地址>
+git push -u origin main
+```
+
+如果是自建的 Gitea / GitLab,地址形如 `http://192.168.1.10:3000/team/course-factory.git`。
+纯本机的话,也可以直接指向本地目录的 bare 仓库:
 
 ```bash
-git remote add origin <地址>
+git init --bare D:/git/course-factory.git
+git remote add origin D:/git/course-factory.git
 git push -u origin main
 ```
 
+**不要推到公开 GitHub** —— 库里有客户名称、报价、内部方法论。
+
 ---
 
 ## 大文件