graphify 的 add 与 --watch 深度解析:URL 内容摄入知识图谱与文件夹监听自动更新
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
graphify 除了对本地代码、文档、SQL、配置做确定性 AST 解析外,还提供两条把"外部世界"纳入知识图谱的通道:/graphify add <url>将任意 URL(视频、推文、arXiv 论文、PDF、图片、网页)抓取落盘到./raw语料库,以及--watch文件夹监听器在文件变动时自动重建图谱。本文以 graphify 的 skill 参考文档(add-watch.md)为骨架,结合 graphify/ingest.py 与 graphify/watch.py 的源码实现,完整讲解这两条通道的命令用法、URL 类型自动分派机制、防抖与双路更新策略,以及needs_update标志如何与/graphify --update形成闭环。
这两个入口在 skill 体系中的定位
参考文档开头就明确了加载条件:只有当用户执行了/graphify add <url>或传入了--watch时,Agent 才需要加载这份参考文档,二者都不属于默认构建流程(原文:"Neither is part of the default build")。也就是说,一次普通的 graphify 构建只扫描本地目录;add与--watch是两个可选项,分别解决"语料从哪来"与"图谱如何保鲜"的问题。
两个命令都通过同一行模板调用:
$(cat graphify-out/.graphify_python) -c "..."其中graphify-out/.graphify_python是 graphify 构建时写入的解释器标记文件,保证 Agent 使用的是安装 graphify 的那个 Python 环境(graphify/hooks.py 中的 hook 逻辑也读取同一标记来定位解释器)。这一点在下面的两段命令中同样适用。
/graphify add:把 URL 摄入语料库
命令模板与替换规则
文档给出的完整命令如下,需要替换三个占位符:
$(cat graphify-out/.graphify_python) -c " import sys from graphify.ingest import ingest from pathlib import Path try: out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') print(f'Saved to {out}') except ValueError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) except RuntimeError as e: print(f'error: {e}', file=sys.stderr) sys.exit(1) "URL:要摄入的实际地址;AUTHOR:用户提供时填入其姓名,否则留空;CONTRIBUTOR:团队图谱场景下的贡献者名,同样可选。
文档同时规定了两条行为约束:
- 出错必须显式上报——命令以错误退出时,要告诉用户哪里出了问题,而不是静默继续(对应上面捕获
ValueError/RuntimeError并写 stderr、退出码 1 的处理); - 成功后自动续跑
--update——文件保存成功后,立即在./raw上运行--update流水线,把新文件合并进已有图谱,而不需要用户再手动触发。
源码印证:ingest() 的分派流程
上述命令最终调用的是 graphify/ingest.py 中的ingest(url, target_dir, author, contributor)。其执行顺序在源码中非常清晰:
- 确保目标目录存在(
target_dir.mkdir(parents=True, exist_ok=True)); - 调用
_detect_url_type(url)分类 URL; - 调用
validate_url(url)做安全校验(来自 graphify/security.py); - 按类型走不同分支,网络异常统一包装为
RuntimeError,URL 非法则抛ValueError——这正与命令模板中捕获的两种异常类型一一对应。
URL 类型自动检测
参考文档列出的类型表("Supported URL types, auto-detected")与源码_detect_url_type()(graphify/ingest.py)完全吻合,检测规则是纯字符串/后缀判断:
| URL 类型 | 检测规则 | 落盘形式 | 后续处理 |
|---|---|---|---|
| YouTube / 任意视频 | 含youtube.com或youtu.be | 音频文件(.m4a/.opus等) | 下次构建时由 Whisper 转写为.txt,需要pip install 'graphifyy[video]' |
| Twitter / X | 含twitter.com或x.com | .md(YAML frontmatter + 推文正文与作者) | 通过 oEmbed 抓取 |
| arXiv | 含arxiv.org | .md(摘要 + 元数据) | 通过 export API 抓取摘要 |
路径以.pdf结尾 | .pdf原文件 | 直接下载二进制 | |
| 图片(.png/.jpg/.webp 等) | 路径以图片后缀结尾 | 原格式图片文件 | 下次构建时由 Claude vision 抽取 |
| 任意网页 | 以上均不命中 | .md(frontmatter + 正文 markdown) | 默认兜底分支 |
几个值得注意的实现细节(均来自 graphify/ingest.py 源码):
- 视频分支:
url_type == "youtube"时延迟导入from graphify.transcribe import download_audio(graphify/transcribe.py),用 yt-dlp 下载"最佳音频流",文件名基于 URL 的 SHA-1 前 12 位生成(yt_<hash>.<ext>),已存在同名文件时直接命中缓存、跳过下载。对应的安装依赖即 pyproject.toml 中的可选依赖组:video = ["faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9"]。 - 推文分支:先把
x.com归一化为twitter.com,再请求publish.twitter.com/oembed接口取html与author_name;oEmbed 失败时不报错,而是写入一条 "could not fetch content" 的占位说明,保证 URL 至少以存根形式进入语料库(_fetch_tweet,graphify/ingest.py)。 - arXiv 分支:用正则
(\d{4}\.\d{4,5})从 URL 提取论文 ID,改向export.arxiv.org/abs/<id>抓取标题、作者与摘要;提取不到 ID 时降级为普通网页处理。文件名固定为arxiv_<ID>.md(点号换下划线),与 URL 形态无关,便于去重识别(_fetch_arxiv,graphify/ingest.py)。 - 网页分支:先提取
<title>,再做 HTML → Markdown 转换。参考文档写的是"via html2text",而从源码看,_html_to_markdown()实际优先使用 markdownify(若已安装),否则回退到"去标签 + 空白折叠"的兜底实现(截断 8000 字符);正文最终以markdown[:12000]写入文件,控制单个抓取页面的规模上限(graphify/ingest.py)。 - 所有文本类落盘文件都带 YAML frontmatter,记录
source_url、type、author/title/arxiv_id、captured_at(UTC ISO 时间)与contributor——这些字段会在后续提取时成为图谱节点的元数据,--author/--contributor参数就是在这里生效的。
安全与命名细节
ingest()路径上有几道在参考文档中未展开、但值得了解的保护措施:
- URL 校验:所有抓取前都经过
validate_url(),在请求发出前拦截私有 IP 与非法 scheme(SSRF 防护;graphify/transcribe.py 中同样在下载前调用并附有注释说明)。 - 文件名净化:
_safe_filename()把netloc + path中所有非[\w-]字符替换为_、折叠连续下划线、截断到 80 字符,避免抓取路径写入非法文件名(graphify/ingest.py)。 - 防覆盖:目标文件已存在时自动追加
_1、_2… 计数后缀(上限 1000),不会静默覆盖语料库中的旧文件(graphify/ingest.py)。 - YAML 注入防护:抓取来的标题/作者等外部字符串一律经过
_yaml_str()转义后再嵌入 frontmatter,覆盖\n、\r、\t、\0以及 U+2028/U+2029 等所有 YAML 行分隔符,防止恶意页面标题"逃逸"出双引量标注入兄弟键(graphify/ingest.py)。
--watch:文件夹监听与自动更新
命令模板
参考文档给出的第二条命令:
$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3把INPUT_PATH替换为要监听的文件夹即可。--debounce的单位是秒,默认值 3 秒。
双路更新策略:按"变了什么"决定怎么做
这是 watch 的核心设计,文档将其表述为两种行为,源码在 graphify/watch.py 的watch()函数中有逐行对应:
- 只有代码文件变更(.py、.ts、.go 等):立即重跑"AST 提取 + 重建 + 聚类",全程不需要 LLM,
graph.json与GRAPH_REPORT.md自动更新。源码中这一步由_rebuild_code(watch_path)执行,内部串联detect → extract → build → cluster → analyze → report → to_json(graphify/watch.py 中的导入清单即为该调用链)。 - 文档、论文或图片变更:写一个
graphify-out/needs_update标志文件并打印提示,告知需要运行/graphify --update才能完成 LLM 语义重抽取。这一步由_notify_only()实现(graphify/watch.py),它只写标志、不尝试重建——因为语义层节点无法用纯 AST 路径再生。
两条路径的判定函数分别值得看一下(graphify/watch.py):
_batch_triggers_rebuild(batch):批内任一文件后缀属于代码扩展名,或任一文件已被删除时立即重建。注意"任意文件删除也触发重建"这一细节:删除后的陈旧节点淘汰(eviction)同样不需要 LLM,若只按"是否有代码变更"判断,一个纯文档删除批就会被搁置在needs_update标志后面。_batch_needs_llm_flag(batch):只有仍然存在于磁盘上的非代码文件才需要写needs_update标志;已删除的非代码文件交给重建时的对账清扫处理,纯删除批不会留下陈旧标志。
另外源码还有一层文档未提的兜底:_rebuild_code用 per-repo 的flock咨询锁(graphify-out/.rebuild.lock)加.pending_changes排队文件保护重建过程,避免多进程并发重建互相覆盖(graphify/watch.py)。
防抖(Debounce)机制
参考文档对防抖的解释是:"等到文件活动停歇后才触发,避免并行 Agent 写入的浪潮按文件逐个触发重建"。源码实现(graphify/watch.py)与此描述一一对应:
- 主循环每 0.5 秒醒一次;
- 事件到来时只记录
last_trigger = time.monotonic()并把路径累加进changed集合,不立即重建; - 只有当
pending为真且距离最后一次触发已不小于debounce秒时,才把整批路径取出、清空集合,然后打印N file(s) changed并执行上述双路判定。
因此--debounce 3的含义是"最后一次文件变动后静默 3 秒",一波连续写入只会产生一次重建。CLI 参数解析见 graphify/watch.py:位置参数path缺省为.,--debounce为浮点数、默认 3.0。
事件过滤:watch 到底看什么
watch()基于 watchdog 库,在注册 handler 之前有一组过滤规则(graphify/watch.py):
- 扩展名白名单:
_WATCHED_EXTENSIONS是代码、文档、论文、图片四类扩展名的并集(CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS,来自 graphify/detect.py),白名单之外的事件直接丢弃; .graphifyignore优先短路:模式在启动时只解析一次,每个事件先查忽略规则再查扩展名,避免高负载卷上的无效事件耗尽 CPU;- 点目录与输出目录:路径中任何一段以
.开头、或位于graphify-out/内的变更一律忽略——后者尤其重要,否则重建产物本身又会被当成输入,形成自我触发; - 只读事件过滤:Linux 上 inotify(watchdog ≥ 2.3 / ≥ 4)会对每次打开/关闭文件产生
opened、closed_no_write事件,包括 watcher 自己读文件、hook stat 源码在内;源码将这两类显式视为"读"而非"变更",只把创建、修改、移动、删除与 close-after-write 当作变更(_is_read_only_event,graphify/watch.py); - macOS 特殊处理:
sys.platform == "darwin"时改用PollingObserver,因为 FSEvents 在某些编辑器下会漏掉快速保存(graphify/watch.py)。
watch 启动时的标准输出也值得留意,它会直接告诉用户两条通道各自的语义:
[graphify watch] Watching <abs-path> - press Ctrl+C to stop [graphify watch] Code changes rebuild graph automatically. Doc/image changes require /graphify --update. [graphify watch] Debounce: 3.0s按文档说明,Ctrl+C即可停止,handler 捕获KeyboardException后打印Stopped.并停止 observer。
面向 Agent 工作流的用法
参考文档最后一段给出了在 Agent 化流程中的部署建议:把--watch放在后台终端运行。这样 Agent 分"波"(wave)写代码时,波与波之间的代码改动会被监听器自动拾取并重建图谱;但如果同一批 Agent 还写了文档或笔记,那些变更只会留下needs_update标志,需要在相应波结束后手动补一次/graphify --update。这与上一节的双路策略是同一逻辑的使用侧表述。
needs_update 标志的闭环:check-update 与读取提示
needs_update标志不是只写不读的产物,graphify 为它提供了消费端:
graphify check-update <path>子命令:检查标志是否存在,存在则打印"有待处理的非代码变更,请运行/graphify --update";它总是返回成功(cron-safe),适合放进定时任务轮询(命令帮助见 graphify/main.py,实现check_update只读标志、不清除——清标志是执行--update的职责)。- 读取钩子:Agent 通过 Read 工具读取项目文件时,graphify/cli.py 中的 hook 逻辑会把
graphify-out/needs_update的存在视为"图谱已陈旧"的信号,向 Agent 提示图谱落后于磁盘状态,从而引导其在问答前先触发更新。
这两个消费端保证了"文档变更后忘记跑--update"这件事最终会被系统自己发现,而不是静默地用旧图谱回答问题。
测试覆盖
上述行为均有对应测试可供查证:
- tests/test_ingest.py:URL 分类、抓取落盘与异常路径;
- tests/test_watch.py:
needs_update标志的写入/读取、check_update不删标志、代码/文档混合批次的分发判定,以及用debounce=0.2在真实文件系统事件下驱动watch()的端到端用例; - tests/test_transcribe.py:视频音频下载与转写;
- tests/test_security.py:
validate_url等抓取前安全校验。
适用前提与限制
结合 pyproject.toml 与源码,使用本文两条通道时的适用前提如下:
add的可选能力依赖 extras:视频转写需要pip install 'graphifyy[video]'(提供 yt-dlp 与 faster-whisper,且 faster-whisper 要求 Python ≥ 3.11);网页→Markdown 的质量取决于是否安装了 markdownify,否则回退到基础去标签实现并截断正文。- 抓取目标必须通过安全校验:私有地址、非法 scheme 会在发出请求前被拒绝,
add因此无法用于摄入内网资源 URL。 --watch依赖 watchdog:缺失时watch()会明确报出安装提示而非静默失败;macOS 上自动切换为轮询观察器,行为与 inotify 略有差异。- watch 只负责"代码层保鲜"与"语义层提示":文档/论文/图片的语义节点永远需要
/graphify --update的 LLM 流水线补齐,这是设计上的边界,不是缺陷。
综上,graphify 的add与--watch一条解决语料入口(任意 URL 类型自动分派、安全抓取、防覆盖落盘),一条解决图谱时效(防抖批处理、代码变更免 LLM 即时重建、非代码变更以标志文件提示语义更新),二者共同让"任何代码库 + 其文档"构成的知识图谱可以随时间与外部信息持续生长。
【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考