首页
/ graphify 增量内容接入指南:/graphify add 抓取 URL 与 --watch 目录监听自动重建图谱

graphify 增量内容接入指南:/graphify add 抓取 URL 与 --watch 目录监听自动重建图谱

2026-09-06 19:07:24作者:傅爽业Veleda

graphify 默认构建流程只处理「磁盘上已有的语料」,而 addwatch 是两种非默认构建能力:前者让 Agent 把一条 URL(网页、论文、推文、视频、PDF、图片)拉取进 ./raw 语料并触发增量更新,后者让目录监听守护进程在文件变化时自动重建 graph.jsonGRAPH_REPORT.md。本文以技能参考文档 add-watch.md 为主干,结合 ingest.pywatch.pydetect.py 等源码,完整讲解两种能力的命令用法、URL 分派规则、错误处理契约与底层实现,帮助你在 Agent 工作流中正确调用并理解其行为边界。

一、能力定位:不属于默认构建的可选路径

在 graphify 的技能体系中,参考文档 add-watch.md 仅在两种场景下被加载:

  • 用户执行了 /graphify add <url>
  • 用户为某个目录传入了 --watch 参数。

也就是说,常规的「对已有文件夹做全量提取、建图、聚类」流程不会触达该文档;它解决的是语料增量供给问题:既要让外部 URL 成为可查询图谱的一部分,也要让运行中不断变化的文件夹(尤其是 Agent 多轮写入的目录)自动保持图谱新鲜。

skill-agents.md 可以看到,graphify 技能安装阶段会把当前解释器路径写入 graphify-out/.graphify_python(对应 sys.executable),随后所有 bash 片段都通过 $(cat graphify-out/.graphify_python) 取回正确解释器。add-watch 参考文档中的命令同样遵循这一约定——它保证脚本始终跑在安装 graphify 的同一个 Python 环境里,而不是 shell 默认的任意 python3

二、/graphify add:抓取 URL 并入语料

2.1 命令模板与参数替换

参考文档给出的核心命令如下:

$(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 要抓取的真实链接 交给 ingest() 自动判定类型(见下文)
AUTHOR 用户名 用户提供了姓名时填写,未提供时为可选
CONTRIBUTOR 贡献者名 团队图谱场景下记录是谁把内容加进来的

成功后命令会打印 Saved to <路径>;若失败则打印 error: ... 到 stderr 并以退出码 1 结束。参考文档明确了失败契约:出错必须向用户说明原因,不能静默继续

抓取成功并落盘后,Agent 需要自动在 ./raw 上运行 --update 增量管线,把新文件合并进已有图谱。这与 update.md 描述的增量流程衔接:detect_incremental 只挑出「新增/变更」文件做重提取(AST 层不消耗 LLM token),再经 build_merge 合并、save_manifest 落盘清单,保证下一次 update 的 diff 基线正确。换句话说,add 只是「把内容搬进语料」,真正把内容变成图谱节点/边的是随后的 --update

2.2 支持 URL 类型与自动识别

参考文档列出六类 URL(自动检测),并与 ingest.py_detect_url_type()第 64–81 行)的判定顺序一一对应:

URL 类型 识别规则 抓取与落盘方式
Twitter/X 域名含 twitter.comx.com 经 oEmbed 接口抓取,存为 .md,含推文正文与作者
arXiv 域名含 arxiv.org 抓取摘要页并存为 .md(摘要 + 元数据)
YouTube 域名含 youtube.com / youtu.be yt-dlp 下载纯音频,下次运行时转写为 .txt
PDF 路径以 .pdf 结尾 二进制下载,保存为 .pdf
图片 后缀为 .png/.jpg/.jpeg/.webp/.gif 二进制下载,下次运行时由 Claude vision 提取
普通网页 其余所有情况 html2text 风格转换存为 .md

需要留意的实现细节:_detect_url_type 还会把含 github.com 的链接单独标记为 github 分支,但 ingest() 主流程并未为它走专用提取器,而是落到通用网页抓取。此外图片/PDF 这类二进制文件只是「下载进语料」,真正的语义理解发生在下一次 --update/提取运行时——文档明确说明了这一点,这也是为什么文档要求 add 成功后自动补跑 update 管线。

2.3 分类型抓取的实现依据

推文(tweet)_fetch_tweet()ingest.py 第 103–133 行)先把 x.com 规范化为 twitter.com,再请求 https://publish.twitter.com/oembed?url=...&omit_script=true;从返回的 html 字段剥离标签取正文、用 author_name 记作者。oEmbed 失败时降级保存一条「无法获取内容」的占位标记,不会让 add 命令整体崩溃。落盘文件带 YAML frontmatter:source_urltype: tweetauthorcaptured_atcontributor

网页(webpage)_fetch_webpage()ingest.py 第 136–162 行)用正则抓 <title>,正文经 _html_to_markdown() 转换——先无条件剥离 <script>/<style>(避免脚本文本泄漏进图谱),再优先用 markdownifyheading_style="ATX"bullets="-"strip=["img"]);未安装 markdownify 时回退到基础标签剥离并把正文截断到 8000 字符。正文主体最终截断至 12000 字符。

arXiv_fetch_arxiv()ingest.py 第 165–207 行)用正则 (\d{4}\.\d{4,5}) 提取论文编号,命中则请求 https://export.arxiv.org/abs/<id>,再以选择器正则分别抽取标题、摘要、作者;文件名按 arxiv_<id 以_替换.>.md 生成,frontmatter 中记录 type: paperpaper_authors。若 URL 中识别不到编号则回退为普通网页抓取。

YouTube/视频ingest()download_audio()transcribe.py 第 50–92 行)。文件名不取 yt-dlp 的 %(title)s(可能含怪异字符),而是以 URL 的 SHA-1 前 12 位生成稳定文件名 yt_<hash>.<ext>,并先探测 .m4a/.opus/.mp3/.ogg/.wav/.webm 缓存避免重复下载。yt-dlp 选项固定为 bestaudio[ext=m4a]/bestaudio/bestnoplaylist、不做 ffmpeg 后处理。后续转写由 faster-whisper 完成,依赖需要在 pyproject.toml 的可选依赖中按 pip install 'graphifyy[video]' 安装(对应 video = ["faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9"])。

PDF 与图片_download_binary()ingest.py 第 210–215 行)经 safe_fetch 直接写字节,图片会补全缺失的扩展名。

2.4 健壮性设计:安全、命名、防覆盖

  • 安全校验ingest() 在任何网络请求前调用 validate_url(url)security.py 第 103 行),拦截私有 IP、非法 scheme 等恶意目标;抓取统一经 safe_fetch/safe_fetch_text(默认超时 15 秒、字节上限受限)完成。
  • YAML 注入防护:抓取回来的标题/作者是「外部不可信字符串」,直接拼进 frontmatter 可能导致 YAML 逃逸。_yaml_str()ingest.py 第 13–52 行)手工实现双引号标量的完整转义,覆盖 \\"、换行、TAB、NUL、U+2028/U+2029 以及全部控制字符,且刻意不依赖 PyYAML(它不在项目运行时依赖中)。
  • 文件名安全_safe_filename()ingest.py 第 55–61 行)取 netloc + path,把非 \w- 字符折叠为 _ 并截断到 80 字符。
  • 防覆盖:落盘前若同名文件已存在,则循环追加 _1_2… 计数器(上限 1000),保证重复 add 同一类内容不会互相覆盖。

顺带一提,ingest.py 还支持直接以模块方式运行:python -m graphify.ingest <url> [target_dir] [--author X] [--contributor Y],与技能内嵌命令行为一致,适合在无 Agent 环境下调试。

三、--watch:目录监听与自动更新

3.1 命令模板

$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3

INPUT_PATH 替换为要监听的目录。watch.py__main__ 解析参数:path 默认 .--debounce 为浮点数、默认 3.0 秒(watch.py 第 2240–2247 行)。进程启动后会打印监听路径与去抖窗口,按 Ctrl+C 优雅退出(watch() 捕获 KeyboardInterrupt 后调用 observer.stop()/join())。

3.2 触发后的两类行为

参考文档把监听行为按变更文件类型分成两条路径,源码 watch.py 第 2149–2159 行 的 docstring 与之完全对应:

  • 纯代码变更(.py/.ts/.go 等):立即重跑 AST 提取 + 重建 + 聚类,全程不需要 LLMgraph.jsonGRAPH_REPORT.md 自动更新。重建完成时会打印 [graphify watch] Rebuilt: N nodes, M edges, C communities,并列出本轮更新的产物(graph.json、可选的 graph.html、GRAPH_REPORT.md、以及此前生成过的 *-callflow.html)。
  • 文档/论文/图片变更:不立即重建,而是在 graphify-out/needs_update 写入标记并打印通知,提示运行 /graphify --update——因为这类文件需要 LLM 语义重提取,watcher 本身不承担。

判定逻辑在 _batch_triggers_rebuild()watch.py 第 2107–2118 行):批内存在代码文件或存在删除事件即触发重建——删除无需 LLM,纯文档删除也能被 reconcile 清扫掉,不会空等标记;_batch_needs_llm_flag() 则只对「仍然存活于磁盘」的非代码文件写 needs_update 标记,纯删除批不会留下脏标记。

3.3 去抖(Debounce)机制

参考文档强调去抖默认 3 秒:等待文件活动静止后再触发,从而避免 Agent 并行写入的一波文件按文件逐个重建。源码中的实现是事件处理器只记录 last_trigger = time.monotonic() 并把路径放入 changed 集合;主循环每 0.5 秒轮询一次,仅当 pending 为真且距最后一次事件超过 debounce 秒时才取出整个批处理(watch.py 第 2221–2232 行)。整波并行写入因此被合并为一次重建。

3.4 监听范围与过滤器

watcher 并非监听目录下一切变化,而是受三个层次约束(见 watch.py 第 2183–2208 行Handler):

  1. 扩展名白名单_WATCHED_EXTENSIONS = CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS,这些集合定义在 detect.py 第 44–47 行。其中代码扩展覆盖 .py/.ts/.js/.go/.rs/.java/.c/.cpp/.h/.swift/.kt/.cs/.rb/.php/.sql/.sh/.bash/.json/.tf/.xaml/.razor/.sln 等数十种;文档为 .md/.mdx/.qmd/.skill/.txt/.rst/.html/.yaml/.yml;论文为 .pdf;图片为 .png/.jpg/.jpeg/.gif/.webp/.svg
  2. 忽略规则:启动时一次性加载 .graphifyignore(并尊重持久化的 gitignore 设置),事件回调里不再反复解析——避免忙卷上每个 OS 事件都做一次完整解析而烧满 CPU。点开头文件(如 .git/、隐藏目录)与 graphify-out 输出目录本身也被排除,防止 watcher 监听自己写入的产物造成死循环。
  3. 只读事件过滤:Linux 上 inotify(watchdog ≥ 2.3)会对每次 open/close 上报 openedclosed_no_write,若不加过滤,watcher 自身的重建读文件就会触发下一次重建,无限自激。_is_read_only_event()watch.py 第 2121–2136 行)只把创建/修改/移动/删除/写后关闭视为变更;macOS 因使用 PollingObserver 不产生此类事件,过滤成为空操作。

3.5 重建的并发与资源约束

--watch 重建不是「裸跑」,而是经过一整套守护机制(见 watch.py 第 161–246 行):

  • 仓库级重建锁:基于 fcntl.flock_rebuild_lock() 保证同一输出目录同时只有一个重建进程,锁文件 .rebuild.lock 内写入持有者 PID;进程被杀锁会自动释放,无需清理残留。Windows(无 fcntl)退化为空操作。
  • 待处理变更排空:拿不到锁的进程不会丢变更集——_queue_pending() 把路径追加到 .pending_changes(append 模式、逐行写入保证并发不互相覆盖),持锁进程在重建前后排空该文件并去重合并(_drain_pending/_merge_changed_paths,对应 issue #1059)。
  • 资源限制:重建进程 best-effort os.nice(10) 降优先级,并可通过环境变量 GRAPHIFY_REBUILD_MEMORY_LIMIT_MB 设置内存上限(Linux 用 RLIMIT_AS,macOS 用 RLIMIT_DATA,因为 Apple libmalloc 下 AS 限制不可靠)。
  • 构建配置持久化--exclude/gitignore 等语料整形选项会写入输出目录旁的 .graphify_build.json,让 update/watch/hook 触发的重建复用同一套排除规则,避免增量重建悄悄把被排除路径重新纳入(对应 #1886)。

3.6 对语义文件的延迟语义化处理

对新增/变更的视频文件,参考 transcribe.md:raw .mp4/.mp3 无法被语义子代理直接读取,update 管线会先经 transcribe_all() 转为 .txt 并当作 document 处理。watcher 不承担这一层——它只负责对代码事件即时重建、对非代码事件举手示意,语义化统一交给 /graphify --updatecheck_update()watch.py 第 2077–2089 行)则是一个 cron 安全的探针函数:检测到 needs_update 标记就提示运行 update,且永远返回 True,不会让定时任务误报。

四、Agentic 工作流中的组合用法

参考文档最后给出 agentic 场景的实战指引,是理解 add + watch 设计动机的关键:

--watch 跑在后台终端里。Agent 每波写入之间的代码变更会被自动拾取;若 Agent 同时也在写文档或笔记,则在那几波结束后需要手动 /graphify --update

由此形成一套完整的「自维护语料」闭环:

  1. 代码波:Agent 多进程并行改动 .py/.ts/... → watcher 去抖合并为一次 AST 重建(无 LLM、零 token 成本),graph.json/GRAPH_REPORT.md 即时保持可用。
  2. 文档波:Agent 新增笔记、接入外部资料时 → watcher 写入 needs_update 标记并提示;人工或上层编排在波结束后执行一次 /graphify --update,对这批文档/图片/视频做 LLM 语义提取。
  3. 外部 URL:想纳入不在磁盘上的资料时 → /graphify add <url> 把网页/推文/论文/PDF/图片拉进 ./raw(视频先下载音频),随后紧跟一次 --update,让它进入图谱。

这样安排的本质原因是 graphify 的双层提取模型:AST 提取是确定性的、可离线即时完成的,而文档/图片/视频的语义提取依赖 LLM,应当批量、集中、按需触发。add 负责供给、watch 负责感知、--update 负责语义化,三者各司其职,正好拼出增量更新的完整链路。

五、实践要点小结

  • addwatch非默认能力,技能在检测到 /graphify add--watch 时才加载 add-watch.md,不要把它们当全量构建的替代品。
  • 命令中的解释器一律用 $(cat graphify-out/.graphify_python),确保与安装 graphify 的环境一致;/graphify add 出错时向用户报告原因并以退出码 1 结束,不得静默吞掉失败
  • add 成功落盘后必须补跑一次 --update(参考 update.md 的增量流程),否则新文件不会进入图谱。
  • 视频转写需先 pip install 'graphifyy[video]'(faster-whisper + yt-dlp),转写提示词经 GRAPHIFY_WHISPER_PROMPT 环境变量传入,模型名用 GRAPHIFY_WHISPER_MODEL(默认 base)。
  • --watch 只对代码变更即时重建;文档/论文/图片变更只落 graphify-out/needs_update 标记。等待这类更新时检查该标记即可,不必反复轮询进程输出。
  • 去抖默认 3 秒、--debounce 可调;macOS 走 PollingObserver(防 FSEvents 漏事件),Linux 用原生 watchdog;两者都内置了只读事件、点开文件与 graphify-out 的自排除,避免自激循环。
登录后查看全文
热门项目推荐
相关项目推荐