mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:41:58 +00:00
2714890d0d
README.zh-CN.md / README.md 只留:这是什么、为什么用它、主要功能(按领域一行一条,链到文档)、快速开始、部署、文档索引、 开发与测试、许可(原文不动)。原来的每一节按读者和任务分到 17 篇主题文档:ask-data、workflows、evidence、 data-sources(新增:接入数据库、只读保证、上传表格、跨库合并、遮罩列)、data-catalog、tools(新增:工具、审批档位、 能力登记、端点、网络规则——它们管所有工作流,不只应用)、apps、evaluation-and-ledger、schedules(含通知)、deployment、 configuration、postgresql-and-scaling、identity-and-security、architecture、development、upgrade-notes(按时间倒序, 每波先写要做的事)、faq。删掉的只有模块名(api/copilot_gen.py、chat/drive.py、data/schema_view.py)、重复的话和 「以前…现在…」的叙述;操作步骤、命令、配置名、限制、权限边界都有归宿。中文是原文,英文按新的中文写;英文里界面文字 只写英文名,开头注明界面目前只有简体中文,中文占位符换成英文占位符,中文数字的例子改成不依赖中文的说法。 数据剖析可以同时剖析 2–3 张表(baf23998)收进 data-catalog。 新增 scripts/check-docs.py:英文文档(README.md、docs/en/**)不许有中日韩字符和全角符号(代码块也算), README、docs/**、deploy/README.md 的相对链接和锚点都要通,docs/zh-CN 与 docs/en 文件一一对应、标题层级相同。 中文提示里指向 README 章节的地方改指新文档:cluster/guard.py、cluster/version.py、db/base.py 两处报错, data/schema_view.py 文档串,.env.example 三处,scripts/prod.sh、scripts/test-postgres.sh,terms.ts 两处、 DataSourcesTab.tsx、check-manage.mjs 注释,deploy/README.md 三处链接。deploy/agentlab.env 未动。 验收:check-docs 0 问题(英文文档 18 个 0 处中文,相对链接 369 条 0 断链,18 对标题层级一致);相关后端测试 117 通过、7 跳过;tsc -b、check-copy 通过。
138 lines
5.7 KiB
Python
138 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
||
"""文档检查:英文文档不许有中文、链接和锚点都要通、中英两套文档一一对应。
|
||
|
||
python3 scripts/check-docs.py
|
||
|
||
1. README.md 和 docs/en/** 里不许出现中日韩字符和全角符号(代码块也算),范围是
|
||
U+3000–U+303F、U+3400–U+4DBF、U+4E00–U+9FFF、U+F900–U+FAFF、U+FF00–U+FFEF。
|
||
2. README.md、README.zh-CN.md、docs/**/*.md、deploy/README.md 里的相对链接:文件要存在,带 #锚点 的要对得上
|
||
目标文件的标题(GitHub 的规则:小写,去掉字母、数字、空格、连字符、下划线以外的字符,空格换成 -,同名标题
|
||
依次加 -1、-2)。代码块里的标题不算标题,代码块和行内代码里的链接不算链接。
|
||
3. docs/zh-CN 和 docs/en 的文件名一一对应,每一对的标题层级序列相同。
|
||
|
||
有问题时逐条列出、退出码 1。
|
||
"""
|
||
import os
|
||
import re
|
||
import sys
|
||
import unicodedata
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
CJK = re.compile("[ -〿㐀-䶿一-鿿豈--]")
|
||
HEADING = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
|
||
LINK = re.compile(r"\]\(([^)\s]+)\)")
|
||
CODE_SPAN = re.compile(r"(`+)(.+?)\1")
|
||
|
||
|
||
def md_files():
|
||
out = [os.path.join(ROOT, "README.md"), os.path.join(ROOT, "README.zh-CN.md"),
|
||
os.path.join(ROOT, "deploy", "README.md")]
|
||
for base, _dirs, files in os.walk(os.path.join(ROOT, "docs")):
|
||
out += [os.path.join(base, f) for f in sorted(files) if f.endswith(".md")]
|
||
return [p for p in out if os.path.exists(p)]
|
||
|
||
|
||
def slug_of(text):
|
||
text = CODE_SPAN.sub(lambda m: m.group(2), text)
|
||
text = re.sub(r"!?\[([^\]]*)\]\([^)]*\)", r"\1", text)
|
||
text = re.sub(r"[*_]{1,3}([^*_]+)[*_]{1,3}", r"\1", text)
|
||
kept = [ch for ch in text.strip().lower()
|
||
if ch in " -" or unicodedata.category(ch)[0] in "LMN" or unicodedata.category(ch) == "Pc"]
|
||
return "".join(kept).replace(" ", "-")
|
||
|
||
|
||
def scan(path):
|
||
"""返回 (锚点集合, 标题层级序列, 链接 [(行号, 目标)])。"""
|
||
anchors, seen, levels, links = set(), {}, [], []
|
||
fence = None
|
||
with open(path, encoding="utf-8") as f:
|
||
for no, line in enumerate(f, 1):
|
||
s = line.strip()
|
||
m = re.match(r"^(`{3,}|~{3,})", s)
|
||
if m:
|
||
if fence is None:
|
||
fence = m.group(1)[0] * 3
|
||
elif s.startswith(fence):
|
||
fence = None
|
||
continue
|
||
if fence:
|
||
continue
|
||
h = HEADING.match(line.rstrip("\n"))
|
||
if h:
|
||
levels.append(len(h.group(1)))
|
||
base = slug_of(h.group(2))
|
||
slug = base
|
||
while slug in seen:
|
||
seen[base] += 1
|
||
slug = "%s-%d" % (base, seen[base])
|
||
seen[slug] = 0
|
||
anchors.add(slug)
|
||
for target in LINK.findall(CODE_SPAN.sub("", line)):
|
||
links.append((no, target))
|
||
return anchors, levels, links
|
||
|
||
|
||
def main():
|
||
problems = []
|
||
rel = lambda p: os.path.relpath(p, ROOT)
|
||
|
||
# 1. 英文文档里的中文
|
||
english = [os.path.join(ROOT, "README.md")]
|
||
for base, _dirs, files in os.walk(os.path.join(ROOT, "docs", "en")):
|
||
english += [os.path.join(base, f) for f in sorted(files) if f.endswith(".md")]
|
||
cjk_hits = 0
|
||
for path in english:
|
||
with open(path, encoding="utf-8") as f:
|
||
for no, line in enumerate(f, 1):
|
||
for m in CJK.finditer(line):
|
||
cjk_hits += 1
|
||
problems.append("%s:%d:%d 英文文档里有中文字符 %r" % (rel(path), no, m.start() + 1, m.group(0)))
|
||
|
||
# 2. 链接和锚点
|
||
cache = {}
|
||
|
||
def info(p):
|
||
if p not in cache:
|
||
cache[p] = scan(p)
|
||
return cache[p]
|
||
|
||
link_count = 0
|
||
files = md_files()
|
||
for path in files:
|
||
anchors, _levels, links = info(path)
|
||
for no, target in links:
|
||
if re.match(r"^[a-z][a-z0-9+.-]*:", target): # http:、mailto: 之类的外链不查
|
||
continue
|
||
link_count += 1
|
||
file_part, _, frag = target.partition("#")
|
||
dest = os.path.normpath(os.path.join(os.path.dirname(path), file_part)) if file_part else path
|
||
if not os.path.exists(dest):
|
||
problems.append("%s:%d 链接的文件不存在:%s" % (rel(path), no, target))
|
||
elif frag and dest.endswith(".md") and frag not in info(dest)[0]:
|
||
problems.append("%s:%d 锚点对不上标题:%s" % (rel(path), no, target))
|
||
|
||
# 3. 中英两套文档一一对应
|
||
zh_dir, en_dir = os.path.join(ROOT, "docs", "zh-CN"), os.path.join(ROOT, "docs", "en")
|
||
zh = sorted(f for f in os.listdir(zh_dir) if f.endswith(".md")) if os.path.isdir(zh_dir) else []
|
||
en = sorted(f for f in os.listdir(en_dir) if f.endswith(".md")) if os.path.isdir(en_dir) else []
|
||
for name in sorted(set(zh) ^ set(en)):
|
||
problems.append("docs/zh-CN 和 docs/en 的文件对不上:%s 只在一边" % name)
|
||
pairs = sorted(set(zh) & set(en)) + [None]
|
||
for name in pairs:
|
||
if name is None:
|
||
a, b = os.path.join(ROOT, "README.zh-CN.md"), os.path.join(ROOT, "README.md")
|
||
else:
|
||
a, b = os.path.join(zh_dir, name), os.path.join(en_dir, name)
|
||
if info(a)[1] != info(b)[1]:
|
||
problems.append("标题层级对不上:%s %s ↔ %s %s" % (rel(a), info(a)[1], rel(b), info(b)[1]))
|
||
|
||
for p in problems:
|
||
print(p)
|
||
print("英文文档 %d 个,中文字符 %d 处;Markdown 文件 %d 个,相对链接 %d 条;中英文档 %d 对;问题 %d 条"
|
||
% (len(english), cjk_hits, len(files), link_count, len(pairs), len(problems)))
|
||
return 1 if problems else 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|