Commit f824c1bc authored by 刘正强's avatar 刘正强

初始化老客户拜访知识库:40个客户文件夹 + 必填门模板/校验/hook

parent 61566661
# 脚本与 hook 必须保持 LF(Windows CRLF 会破坏 shell hook 与 python 展开)
*.py text eol=lf
*.sh text eol=lf
hooks/* text eol=lf
server-hooks/* text eol=lf
# 文档统一 LF
*.md text eol=lf
# Windows 批量脚本保持 CRLF
*.cmd text eol=crlf
*.ps1 text eol=crlf
# 其余按平台默认
* text=auto
# 老客户拜访知识库 —— Agent 铁律(AGENTS.md)
本文件定义本仓库所承载知识的**格式与准入规则**,供 AI/Agent 遵守。所有上传进 `客户/` 的拜访报告必须合规,否则禁止写入与推送。
## 目录与命名(硬性)
1. 报告文件:`客户/<客户名>/YYYY-MM-DD_客户名_帛丝人员_拜访报告.md`
2. 文件名中的"客户名"必须与所在文件夹名一致;日期用 `YYYY-MM-DD`
3. 每个客户一个文件夹,不得混放其它客户报告。
## 报告必填格式(必填门)
报告必须包含以下五节,每节必须填写**实际内容**(去除 HTML 注释 `<!-- -->` 与占位符 `<…>` 后仍有文字):
- 一、项目背景
- 二、拜访事前准备
- 1. 拜访目标
- 2. 客户情况及决策关系判断
- 3. 事前谈判策略
- 三、本次实际拜访结果
- 四、问题清单及下一步计划
- 五、其他补充
> 缺节、空节、仅注释/占位符、命名/路径不符 → 一律判为不合规。
## 准入动作(铁律)
1. 生成或修改任何 `*_拜访报告.md` 后,**必须**先运行校验:
```bash
python _validation/validate_report.py "<报告路径>"
```
2. 校验失败 → 不得 commit / push,应列出缺哪节、缺什么,让用户补全后再提交。
3. 可整体扫描:
```bash
python _validation/validate_report.py
```
4. 模板以 `_template/拜访报告_模板.md` 为准,逐节为用户生成带注释的草稿,由用户确认后填入真实内容。
5. 报告被视为受控对象:写入即留痕(git),如需改历史请走正常 git 提交,不删信息源。
## 属性默认值
- 客户名、日期、拜访人(帛丝人员):从文件名解析(`日期_客户_人员`),不必在正文重复,但正文元数据行建议保留。
- 报告补充项(如项目负责人、系统、服务状态):为可选项,不作为必填门。
# laokehu-project-kb
# 老客户拜访知识库(laokehu-project-kb)
老客户拜访计划和情况
\ No newline at end of file
本仓库用于集中管理老客户拜访情况:**一个客户一个文件夹**,每个销售/帛丝人员的每次拜访上传一份**符合统一格式**的 Markdown 报告(`*_拜访报告.md`)。不符合格式的上传会被拦截。
## 目录结构
```
laokehu-project-kb/
├── 客户/ # 40 个客户,每客户一个文件夹
│ ├── 丰隆/
│ ├── 华光/
│ └── ... # 见下方客户清单
├── _template/
│ └── 拜访报告_模板.md # 必填格式模板
├── _validation/
│ └── validate_report.py # 必填门校验器(本地扫描/单文件)
├── hooks/
│ └── pre-push # 本地 push 前校验 hook
├── server-hooks/ # 服务器端拦截(需管理员,可选)
│ ├── pre-receive
│ ├── validate_blob.py
│ └── 部署说明.md
├── initialize-hooks.cmd # Windows 一次性启用本地 hook
├── initialize-hooks.ps1
└── 使用说明.md # 销售端操作步骤
```
## 报告格式(必填门)
报告必填五节,每节须填写实际内容(不能只有注释或占位符):
```
一、项目背景 —— 行业、系统、合同/运维周期、当前服务状态
二、拜访事前准备
1. 拜访目标
2. 客户情况及决策关系判断
3. 事前谈判策略
三、本次实际拜访结果
四、问题清单及下一步计划
五、其他补充
```
## 命名与路径约定(强制)
- 文件:`YYYY-MM-DD_客户名_帛丝人员_拜访报告.md`
- 位置:`客户/<客户名>/`(客户名与文件名中的客户名必须一致)
- 示例:`客户/丰隆/2026-08-20_丰隆_戴堂亮_拜访报告.md`
## 校验方式
```bash
# 校验整个客户目录
python _validation/validate_report.py
# 校验单个报告
python _validation/validate_report.py "客户/丰隆/2026-08-20_丰隆_戴堂亮_拜访报告.md"
```
- **本地 git 客户端**:运行一次 `initialize-hooks.cmd`(或 `.ps1`)后,`pre-push` 会在每次 push 前自动校验,不合规则拒绝。
- **GitLab 网页上传**:不经过本地 hook。管理员可部署 `server-hooks/pre-receive`(见 `server-hooks/部署说明.md`)实现瞬时拦截;否则只能事后全库扫描 `validate_report.py` 兜底。
## 客户清单(40 家)
丰隆 华光 诺葳奇 美盛 大地 华信通 阜康机械 大明矿业 联合荣大 上海普拉斯克 SCB 大王精铸 三胖蛋 万鸿双喜 世纪二千 联创杰 宜昌长机 一甸园 中海沥青 Latticework 龙腾 综讯 兆华电子 泰康电子 安徽海德克 锦华 彤程 汇得 知行 思达特 GEO 燕龙 万润 天星 拓臣 众地 康诺 中邮科技 美睫时 景弘盛
# <客户名>项目拜访事前及事后报告
> 内部项目记录|<YYYY-MM-DD>
>
> 客户名称:<客户名> | 拜访人(帛丝人员):<姓名> | 日期:<YYYY-MM-DD>
## 一、项目背景
<!-- 填写:客户所属行业、使用系统、合同/运维周期、当前服务状态(如 服务中 / 免费运维期 / 已到期 / 有尾款问题) -->
## 二、拜访事前准备
### 1. 拜访目标
<!-- 填写:本次拜访想达成的具体目标 -->
### 2. 客户情况及决策关系判断
<!-- 填写:客户当前状况、关键人(业务/财务/IT/决策人)、决策关系与态度 -->
### 3. 事前谈判策略
<!-- 填写:拜访前准备的推进思路、沟通要点、可能的分歧与备选方案 -->
## 三、本次实际拜访结果
<!-- 填写:本次拜访的实际进展、客户反馈、口头/书面承诺 -->
## 四、问题清单及下一步计划
<!-- 填写:遗留问题清单、责任人与完成日期、下一步动作安排 -->
## 五、其他补充
<!-- 填写:其他值得记录的信息,如风险、后续商机、需协调事项 -->
# 丰隆项目拜访事前及事后报告
> 内部项目记录|2026-08-20
>
> 客户名称:丰隆 | 拜访人(帛丝人员):戴堂亮 | 日期:2026-08-20
## 一、项目背景
丰隆为制造业客户,当前使用 SAP PE,运维合同周期 2026.3.1-2027.2.28,当前服务状态为服务中。
## 二、拜访事前准备
### 1. 拜访目标
推动尾款闭环,明确付款路径与承诺日期。
### 2. 客户情况及决策关系判断
业务负责人王总、财务李经理、IT 张工;决策人王总,近期对服务满意,竞争压力来自于另一家运维商。
### 3. 事前谈判策略
先谈价值回顾,再提尾款路径;备选方案为分期付款。
## 三、本次实际拜访结果
客户确认尾款闭环路径,承诺 8 月底前完成审批;提出系统报表优化需求。
## 四、问题清单及下一步计划
1. 报表导出慢 —— 责任人 张工 / 戴堂亮,完成 9 月 15 日。
2. 尾款审批 —— 责任人 李经理,完成 8 月 31 日。
## 五、其他补充
客户有意向续签下一年度运维合同,待下季度再次沟通。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
laokehu-project-kb 拜访报告校验器(必填门)
校验规则(本次范围):
1. 文件必须位于 客户/<客户名>/ 下,且为 *_拜访报告.md
2. 文件名必须符合命名约定:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md,且"客户名"与所在文件夹名一致
3. 必须包含五大必填节标题,且每个必填节都有实际内容(去掉 HTML 注释和占位符后非空)
用法:
python _validation/validate_report.py <报告.md> 校验单个文件
python _validation/validate_report.py --tree 扫描 客户/ 目录下所有报告,汇总结果(默认)
python _validation/validate_report.py --stdin --name xxx.md 从 stdin 读 blob 内容校验(供服务器 hook 用)
可选:--quiet(只打印通过/失败),--json(结构化输出),--report-path <glob>(扫描子集)
退出码:
0 = 全部合规;1 = 存在不合规(单个文件时=该文件不合规)
"""
import re
import sys
import glob
import json
from pathlib import Path
# ---- 配置 ----
ROOT = Path(__file__).resolve().parent.parent # 仓库根
CLIENT_DIR = ROOT / "客户"
FILENAME_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})_(.+)_(.+)_拜访报告\.md$")
TOP_LEVEL_SECTIONS = [
("一、项目背景", "项目背景"),
("二、拜访事前准备", "拜访事前准备"),
("三、本次实际拜访结果", "本次实际拜访结果"),
("四、问题清单及下一步计划", "问题清单及下一步计划"),
("五、其他补充", "其他补充"),
]
# 占位符 / 无意义文本:命中后视为空
HTML_COMMENT = re.compile(r"<!--.*?-->", re.S)
PLACEHOLDER_TAG = re.compile(r"<[^>]*>|【[^】]*】|[[^]]*]|\(填写\)|(填写)|待填|填写|xxx", re.S)
HEADING = re.compile(r"^\s*#{1,6}\s", re.M)
def parse_level_2_headings(text):
"""返回 [(行号, 标题文本)],level>=2 的标题行。"""
items = []
for i, line in enumerate(text.splitlines(), start=1):
m = re.match(r"^\s*(#{2,6})\s+(.*)$", line)
if m:
items.append((i, m.group(2).strip()))
return items
def section_content(text, heading_text):
"""取从指定二级标题开始、到下一个同级/更高级标题之前的正文。"""
lines = text.splitlines()
start = None
for i, line in enumerate(lines):
m = re.match(r"^\s*(#{2,6})\s+(.*)$", line)
if m and normalize(m.group(2)) == normalize(heading_text):
start = i + 1
break
if start is None:
return ""
end = len(lines)
for i in range(start, len(lines)):
m = re.match(r"^\s*(#{2,6})\s+(.*)$", lines[i])
if m and len(m.group(1)) <= 2: # 下一个二级或更高级标题
end = i
break
return "\n".join(lines[start:end])
def normalize(t):
return re.sub(r"\s+", "", t)
def has_fillable_content(block):
"""去掉注释/占位符/标题后,是否还有有效文本。"""
clean = HTML_COMMENT.sub("", block)
clean = PLACEHOLDER_TAG.sub("", clean)
lines = []
for line in clean.splitlines():
line = line.strip()
if not line:
continue
if re.match(r"^#{1,6}\s", line):
continue
lines.append(line)
body = "".join(lines)
body = re.sub(r"\s+", "", body)
# 至少要有 1 个中英文/数字有效字符(排除纯标点符号行)
import unicodedata
meaningful = "".join(ch for ch in body if unicodedata.category(ch).startswith(("L", "N")))
return len(meaningful) > 0
def check_content(text):
errors = []
for heading, _nick in TOP_LEVEL_SECTIONS:
if __import__("re").search(r"^\s*#{1,6}\s*" + re.escape(heading), text, re.M) is None:
errors.append(f"缺少必填节标题:{heading}")
continue
block = section_content(text, heading)
if not has_fillable_content(block):
errors.append(f"必填节未填写:{heading}")
return errors
def check_path(path):
"""返回 (相对仓库根的 parts, 错误列表)。"""
rel = path.resolve().relative_to(ROOT)
parts = rel.parts
if len(parts) == 0 or parts[0] != "客户":
return parts, [f"位置错误:报告应放在 客户/ 目录下(当前:{rel})"]
if len(parts) != 3:
return parts, [f"目录层级错误:应为 客户/<客户名>/<报告.md>(当前:{rel})"]
if not str(parts[2]).endswith(".md"):
return parts, [f"文件扩展名错误:应为 .md(当前:{parts[2]})"]
m = FILENAME_RE.match(parts[2])
if not m:
return parts, [f"文件名不符合命名约定:应为 YYYY-MM-DD_客户名_帛丝人员_拜访报告.md(当前:{parts[2]})"]
if m.group(2) != parts[1]:
return parts, [f"文件名中的客户名与文件夹名不一致:文件=“{m.group(2)}”,文件夹=“{parts[1]}”"]
return parts, []
def validate_file(path, quiet=False):
path = Path(path)
name = path.name
if not path.exists():
return False, [f"文件不存在:{path}"]
problems = []
_, path_errs = check_path(path)
problems += path_errs
text = None
try:
with open(path, "r", encoding="utf-8") as fh:
text = fh.read()
except UnicodeDecodeError:
try:
with open(path, "r", encoding="gbk") as fh:
text = fh.read()
except Exception as e:
return False, [f"无法读取文件内容:{e}"]
if text is not None:
problems += check_content(text)
ok = len(problems) == 0
if not quiet:
if ok:
print(f"[通过] {name}")
else:
print(f"[失败] {name}")
for p in problems:
print(f" - {p}")
return ok, problems
def scan_tree(report_glob=None):
if report_glob is None:
report_glob = "**/*_拜访报告.md"
files = sorted(CLIENT_DIR.glob(report_glob)) if CLIENT_DIR.exists() else []
results = []
for f in files:
ok, problems = validate_file(f, quiet=True)
results.append((f, ok, problems))
return results
def main():
args = sys.argv[1:]
quiet = "--quiet" in args
as_json = "--json" in args
report_glob = None
if "--report-path" in args:
i = args.index("--report-path")
report_glob = args[i + 1]
if "--stdin" in args:
name = None
if "--name" in args:
i = args.index("--name")
name = args[i + 1]
text = sys.stdin.read()
problems = check_content(text)
if name:
m = FILENAME_RE.match(name)
if not m:
problems.append(f"文件名不符合命名约定:{name}")
ok = len(problems) == 0
if as_json:
print(json.dumps({"ok": ok, "name": name, "errors": problems}, ensure_ascii=False))
elif not quiet:
print("通过" if ok else "未通过", name or "stdin")
for p in problems:
print(f" - {p}")
return 0 if ok else 1
if args and not args[0].startswith("--"):
ok, problems = validate_file(args[0], quiet=quiet)
if as_json:
print(json.dumps({"ok": ok, "file": args[0], "errors": problems}, ensure_ascii=False))
return 0 if ok else 1
# 默认:扫描整个 客户/ 树
results = scan_tree(report_glob)
total = len(results)
passed = sum(1 for _f, ok, _p in results if ok)
failures = [(f, p) for f, ok, p in results if not ok]
if as_json:
print(json.dumps({
"total": total,
"passed": passed,
"failed": len(failures),
"failures": [{"file": str(f.relative_to(ROOT)), "errors": p} for f, p in failures],
}, ensure_ascii=False))
else:
print(f"共 {total} 份报告,合规 {passed} 份,不合规 {len(failures)} 份")
for f, p in failures:
print(f"[失败] {f.relative_to(ROOT)}")
for e in p:
print(f" - {e}")
return 0 if len(failures) == 0 else 1
if __name__ == "__main__":
sys.exit(main())
#!/bin/sh
# laokehu-project-kb 拜访报告必填门前置校验 hook
# 由 initialize-hooks 配置 core.hooksPath=hooks 后生效。
# 作用:push 前校验本次推送涉及的拜访报告是否合法;任一不合规则拒绝推送。
RC=0
ROOT=$(git rev-parse --show-toplevel) || exit 1
VALIDATOR="$ROOT/_validation/validate_report.py"
# 收集本次推送涉及的 .md 报告文件
REPORTS=""
while read local_ref local_sha remote_ref remote_sha; do
[ -z "$local_sha" ] && continue
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
# 新分支/首次推送:退化为与本地默认分支的合并基点
BASE=$(git merge-base "$local_sha" origin/master 2>/dev/null || echo "$local_sha")
RANGE="$BASE..$local_sha"
else
RANGE="$remote_sha..$local_sha"
fi
CHANGED=$(git -c core.quotepath=false diff --name-only --diff-filter=ACMRT "$RANGE" 2>/dev/null)
for f in $CHANGED; do
case "$f" in
客户/*_拜访报告.md) REPORTS="$REPORTS
$f" ;;
esac
done
done
# 去重
REPORTS=$(printf '%s\n' "$REPORTS" | sort -u | sed '/^$/d')
if [ -z "$REPORTS" ]; then
exit 0
fi
echo "laokehu 校验:本次推送涉及 $(printf '%s\n' "$REPORTS" | sed '/^$/d' | wc -l | tr -d ' ') 份拜访报告,正在校验…"
for f in $REPORTS; do
if python "$VALIDATOR" "$ROOT/$f" >/dev/null 2>&1; then
:
else
echo "✗ 不合规:$f"
RC=1
fi
done
if [ "$RC" -ne 0 ]; then
echo ""
echo "提交被拒:有拜访报告未满足《必填门》格式要求,请按模板补全后再推送。"
echo " - 五必备节:一、项目背景 | 二、拜访事前准备 | 三、本次实际拜访结果 | 四、问题清单及下一步计划 | 五、其他补充"
echo " - 命名:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md(放置于 客户/<客户名>/ 下)"
fi
exit $RC
@echo off
REM laokehu-project-kb 初始化本地校验 hook(Windows 命令行,双击或在此仓库目录运行)
REM 作用:把 git 的 core.hooksPath 指向仓库内 hooks/,使 pre-push 校验生效。
echo [1/2] 设置 core.hooksPath = hooks
git config core.hooksPath hooks
if errorlevel 1 (
echo 设置失败,请确认已在仓库根目录运行。
exit /b 1
)
echo [2/2] 校验 hook 是否可执行
if exist "%CD%\hooks\pre-push" (
echo 完成。之后每次 push 前会自动校验拜访报告格式。
) else (
echo 警告:未找到 hooks\pre-push,请检查仓库完整性。
exit /b 1
)
echo.
echo 提示:用 git 客户端(命令行/IDE)push 时生效;用 GitLab 网页上传不经过本 hook。
echo 如需网页上传也拦截,请管理员部署 server-hooks\pre-receive(见 server-hooks\部署说明.md)。
# laokehu-project-kb 初始化本地校验 hook(PowerShell)
# 作用:把 git 的 core.hooksPath 指向仓库内 hooks/,使 pre-push 校验生效。
$ErrorActionPreference = "Stop"
Write-Host "[1/2] 设置 core.hooksPath = hooks"
git config core.hooksPath hooks
if ($LASTEXITCODE -ne 0) {
Write-Host "设置失败,请确认已在仓库根目录运行。" -ForegroundColor Red
exit 1
}
Write-Host "[2/2] 校验 hook 是否可执行"
if (Test-Path (Join-Path $PWD "hooks\pre-push")) {
Write-Host "完成。之后每次 push 前会自动校验拜访报告格式。" -ForegroundColor Green
} else {
Write-Host "警告:未找到 hooks\pre-push,请检查仓库完整性。" -ForegroundColor Yellow
exit 1
}
Write-Host ""
Write-Host "提示:用 git 客户端(命令行/IDE)push 时生效;用 GitLab 网页上传不经过本 hook。" -ForegroundColor Cyan
Write-Host "如需网页上传也拦截,请管理员部署 server-hooks\pre-receive(见 server-hooks\部署说明.md)。" -ForegroundColor Cyan
#!/bin/sh
# GitLab 服务器端 pre-receive hook —— laokehu 拜访报告必填门强制
# 说明:需要 GitLab 管理员权限启用;使用前先复制 validate_blob.py 到服务器固定路径并改 VALIDATOR。
# 版本:laokehu-project-kb
# 服务器上 validate_blob.py 的固定路径(管理员按需修改)
VALIDATOR=/opt/laokehu/validate_blob.py
PYTHON=${PYTHON:-python3}
RC=0
BAD_PATHS=""
while read old new ref; do
[ -z "$new" ] && continue
[ "$new" = "0000000000000000000000000000000000000000" ] && continue # 删除引用
if [ "$old" = "0000000000000000000000000000000000000000" ]; then
RANGE=$(git rev-list "$new" 2>/dev/null | tail -1).."$new"
[ "$RANGE" = "..$new" ] && RANGE="$new"
else
RANGE="$old..$new"
fi
CHANGED=$(git -c core.quotepath=false diff --name-only --diff-filter=ACMRT "$RANGE" 2>/dev/null)
for p in $CHANGED; do
case "$p" in
客户/*_拜访报告.md)
if git show "$new:$p" 2>/dev/null | "$PYTHON" "$VALIDATOR" --path "$p" --name "$(basename "$p")" -; then
:
else
echo "✗ [pre-receive] 不合规:$p" >&2
BAD_PATHS="$BAD_PATHS
$p"
RC=1
fi
;;
esac
done
done
if [ "$RC" -ne 0 ]; then
echo "" >&2
echo "===== laokehu 提交被拒 =====" >&2
echo "以下拜访报告未满足《必填门》格式要求,请按模板补全后再提交:" >&2
printf '%s\n' "$BAD_PATHS" | sed '/^$/d' | sed 's/^/ - /' >&2
echo "规则:五大必填节(项目背景/拜访事前准备/本次实际拜访结果/问题清单及下一步计划/其他补充)均须有实际内容;" >&2
echo "命名:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md,位于 客户/<客户名>/ 下。" >&2
exit $RC
fi
exit 0
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
laokehu 拜访报告必填门·服务器端 blob 校验器(自包含,供 GitLab pre-receive hook 调用)
从 stdin 读取单个报告文件内容,--name 传入文件名,--path 传入仓库内相对路径。
退出码 0=合规,1=不合规。
"""
import re
import sys
import json
FILENAME_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})_(.+)_(.+)_拜访报告\.md$")
HTML_COMMENT = re.compile(r"<!--.*?-->", re.S)
PLACEHOLDER_TAG = re.compile(r"<[^>]*>|【[^】]*】|[[^]]*]|\(填写\)|(填写)|待填|填写|xxx", re.S)
HEADING = re.compile(r"^\s*#{1,6}\s", re.M)
TOP_LEVEL_SECTIONS = [
("一、项目背景", "项目背景"),
("二、拜访事前准备", "拜访事前准备"),
("三、本次实际拜访结果", "本次实际拜访结果"),
("四、问题清单及下一步计划", "问题清单及下一步计划"),
("五、其他补充", "其他补充"),
]
def normalize(t):
return re.sub(r"\s+", "", t)
def section_content(text, heading_text):
lines = text.splitlines()
start = None
for i, line in enumerate(lines):
m = re.match(r"^\s*(#{2,6})\s+(.*)$", line)
if m and normalize(m.group(2)) == normalize(heading_text):
start = i + 1
break
if start is None:
return ""
end = len(lines)
for i in range(start, len(lines)):
m = re.match(r"^\s*(#{2,6})\s+(.*)$", lines[i])
if m and len(m.group(1)) <= 2:
end = i
break
return "\n".join(lines[start:end])
def has_fillable_content(block):
import unicodedata
clean = HTML_COMMENT.sub("", block)
clean = PLACEHOLDER_TAG.sub("", clean)
lines = []
for line in clean.splitlines():
line = line.strip()
if not line:
continue
if re.match(r"^#{1,6}\s", line):
continue
lines.append(line)
body = re.sub(r"\s+", "", "".join(lines))
return any(unicodedata.category(ch).startswith(("L", "N")) for ch in body)
def check_content(text):
errors = []
for heading, _nick in TOP_LEVEL_SECTIONS:
if re.search(r"^\s*#{1,6}\s*" + re.escape(heading), text, re.M) is None:
errors.append(f"缺少必填节标题:{heading}")
continue
block = section_content(text, heading)
if not has_fillable_content(block):
errors.append(f"必填节未填写:{heading}")
return errors
def main():
args = sys.argv[1:]
name = None
path = None
if "--name" in args:
name = args[args.index("--name") + 1]
if "--path" in args:
path = args[args.index("--path") + 1]
text = sys.stdin.read()
problems = []
if path and not (path.startswith("客户/") and path.endswith("_拜访报告.md")):
problems.append(f"位置错误:应位于 客户/<客户名>/ 且以 _拜访报告.md 结尾:{path}")
if name:
m = FILENAME_RE.match(name)
if not m:
problems.append(f"文件名不符合命名约定:{name}")
problems += check_content(text)
ok = len(problems) == 0
if "--json" in args:
print(json.dumps({"ok": ok, "path": path, "name": name, "errors": problems}, ensure_ascii=False))
else:
for p in problems:
print(p, file=sys.stderr)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
# 服务器端拦截部署说明(需 GitLab 管理员)
本地 `hooks/pre-push` 只在用户用 git 客户端推送时生效。若有同事用 **GitLab 网页上传**,它不会触发本地 hook。要在服务器端做到"不按格式就不准上传",需由管理员安装 **pre-receive hook**
## 原理
GitLab 在更新 refs 之前运行 `pre-receive`。该脚本对本次推送中新增/修改的 `客户/*_拜访报告.md`,把文件内容(blob)交给 `validate_blob.py` 校验;不合规则返回非零退出码,**拒绝整个 push**。网页上传同样走这段 pre-receive,因此网页上传也能被拦截。
## 部署步骤
1. 在服务器上放置校验器到固定路径(本仓库已带有,也可从此仓库复制):
```bash
# 服务器上
mkdir -p /opt/laokehu
scp server-hooks/validate_blob.py root@<gitlab服务器>:/opt/laokehu/validate_blob.py
```
`server-hooks/pre-receive` 里的 `VALIDATOR=/opt/laokehu/validate_blob.py` 改成实际路径;确认服务器有 `python3`
2. 安装 hook 到对应 GitLab 仓库:
- GitLab **服务器端自定义 hook** 需以管理员在服务器仓库目录放置:
`<gitlab-repository-path>/custom_hooks/pre-receive`(旧版本为 `/custom_hooks`)。
- 或 GitLab 项目 **Settings → Repository → Server hooks**(需 Owner,且版本支持)。
-`pre-receive``validate_blob.py` 复制过去,保证 `pre-receive` 可执行(`chmod +x`)。
3. 验证:随意改乱一份报告格式并 push,应被拒绝;改合规后 push 成功。
## 注意事项
- 文件名/路径校验依赖 `git show <new>:<path>` 能读取 blob;pre-receive 阶段对象已上传,可正常读取。
- 本 hook 为**尽力而为的服务器级拦截**(未在真实 GitLab 服务器实测),部署后请先在测试仓库验证。
- 若服务器无 `python3`,可将 `validate_blob.py` 逻辑改写为脚本内嵌 shell 解析,但建议优先安装 python3。
- 该拦截不替代格式维护:仍应引导同事用模板填写。
## 双保险建议
- 本地 `pre-push`:拦截 git 客户端直推不合规(覆盖大部分)。
- 服务器 `pre-receive`:拦截网页/所有来源(**可选,需管理员**)。
- 定期全库扫描:`python _validation/validate_report.py` 作为兜底。
# 拜访报告使用说明(销售/帛丝人员)
## 一、准备(只需做一次)
1. 用 git 克隆本仓库(SSH 或 HTTP 均可):
```
git clone <仓库地址> laokehu-project-kb
```
2. 进入目录并启用本地校验 hook:
- **Windows 命令行**:双击 `initialize-hooks.cmd`,或在该目录运行 `initialize-hooks.cmd`
- **Windows PowerShell**:`powershell -ExecutionPolicy Bypass -File initialize-hooks.ps1`
- **macOS/Linux**:`git config core.hooksPath hooks`
启用后,每次 push 前会自动校验报告格式,不合规会被拦下。
## 二、填写报告
1. 在 `客户/<客户名>/` 文件夹下新建文件,命名规则(**严格按此格式**):
```
YYYY-MM-DD_客户名_帛丝人员_拜访报告.md
```
例:`客户/丰隆/2026-08-20_丰隆_戴堂亮_拜访报告.md`
2. 建议从 `_template/拜访报告_模板.md` 复制内容再填。
3. 把五节里的 `<!-- 填写:… -->` **替换成真实内容**(删除注释,写入内容)。五节:
- 一、项目背景
- 二、拜访事前准备(含 1.拜访目标 / 2.客户情况及决策关系判断 / 3.事前谈判策略)
- 三、本次实际拜访结果
- 四、问题清单及下一步计划
- 五、其他补充
4. 正文顶部元数据行:`客户名称 / 拜访人 / 日期` 填上。
## 三、提交与推送
```bash
git add 客户/
git commit -m "拜访报告:<客户名> <日期>"
git push
```
- 若格式不合规,push 会被拒绝,屏幕会提示缺哪节/哪项,按提示补全后重新 `git add && git commit && git push`。
- 也可提前自查:
```bash
python _validation/validate_report.py "客户/<客户名>/<文件>.md"
```
## 四、常见问题
- **我用的 GitLab / 网页上传**:网页上传不经过本地 hook,无法自动拦截,我只能在你上传后扫描。建议尽量用 git 客户端 push。
- **想提交但校验说"缺少必填节"**:确认 markdown 里五节标题一个不少,且每节下有真实文字(不是空的注释)。
- **提示客户名不一致**:检查文件名里的客户名是否和所在文件夹名完全一致。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment